Základy programování v Pythonu#

Toto je pracovní verze poznámek posbíraných z mé přípravy na hodiny předmětu Základy programování v Pythonu. Najdete zde vše, co jsme dělali během semestru na cvičeních, a možná i něco navíc.

Text rozhodně není kompletní a zcela určitě obsahuje řadu chyb. Některé části mají charakter spíše bodových poznámek, jiné jsou zárodky uceleného výkladu. Místy poznámky prezentují můj osobní názor a místy se určitě mýlím. Vše se snažím ilustrovat na příkladech, z nichž některé nejsou moc dobré. Druhá část obsahuje pár drobných řešených úloh, ale zatím slouží spíše jako odkladiště, které vyžaduje ještě množství práce.

Užívejte zodpovědně.

Václav Alt

A šíření je zakázáno.

Úvod#

V této kapitole si povíme, co za programovací jazyk Python vlastně, co od něho můžeme očekávat, v čem ho můžeme psát, nainstalujeme si vše potřebné a spustíme první program.

O jazyku#

Podle Wiki je Python:

…a high-level, general-purpose programming language. Its design philosophy emphasizes code readability with the use of significant indentation. Python is dynamically typed and garbage-collected. It supports multiple programming paradigms, including structured (particularly procedural), object-oriented and functional programming. It is often described as a “batteries included” language due to its comprehensive standard library.

Na ta zvýrazněná slova se v průběhu semestru podíváme a řekneme se, co to v kontextu jazyka Python vlastně znamená.

Jazyk Python vyvinul v 80. letech holandský programátor Guido van Rossum a první verzi vydal v roce 1991. Python 2 byl vypuštěn roku 2000 a poté roku 2008 Python 3 - významné přepracování. Podpora Pythonu 2 měla být do roku 2015, ale nakonec se protáhla do roku 2020. Aktuální verze Pythonu je 3.12 (únor 2024).

Note

V čase psaní tohoto materiálu používám verzi 3.10.12. Až na pár drobných výjimek by většina příkladů měla fungoval pro verze >3.8.

Jazyk van Rossum pojmenoval po své oblíbené televizní show Monty Python’s flying circus.

A k čemu se dnes Python vlastně používá? Prakticky ke všemu.

  • Data science

  • scripting

  • scientific computation

  • automation

  • game dev

  • web dev

Python se v současné době řadí mezi nejpopulárnější jazyky a jeho popularita v posledních cca 12 letech stále roste. Důvodů je celá řada, mezi ty nejvýznamnější patrně patří fakt, že již součástí Python Standard Library je obrovské množství nástrojů (“batteries included” na Wiki) a celá řada dalších je k dispozici ke stažení přes různé správce balíčků (např. pip či Anaconda). Python je poměrně snadný na použití, podporuje řadu programovacích paradigmat (procedurální, OOP, funkcionální) a je poměrně čitelný (do jisté míry síla zvyku, ale pravdou je, že se kód nehemží “zbytečnými” znaky, což je umocněno ještě tím, že Python je dynamicky typovaný). Srovnejme definici obdobné funkce v Pythonu, C a ve Fortranu.

# samozrejme lze napsat jeste usporneji, ale pak se vytraci porovnani
def average(array):
    sum = 0.
    for x in array:
        sum += x
    return sum / len(array)
float average(float *array, int n) {
    float sum = 0.0;
    for (int i = 0; i < n; i++) {
        sum += array[i];
    }
    return sum / n;
}
FUNCTION AVERAGE(ARRAY)
    REAL :: A(*)
    REAL :: AVERAGE
    INTEGER :: I, N

    AVERAGE = 0.0
    DO I=1, N
        AVERAGE = AVERAGE + ARRAY(I)
    END DO
    AVERAGE = AVERAGE / N
    RETURN
END FUNCTION AVERAGE

V extrémním případe narazíte na něco takového:

Python developer, who has to write Java.

Kritika#

Kritici nejčástěji uvádějí dva důvody, proč jim Python vadí:

  1. Python je pomalý.

  2. Python je dynamicky typovaný.

Osobně jsem poněkud alergický na stížnosti na rychlost Pythonu. Ačkoliv existují dobré důvody, proč běžné operace kompilovaný jazyk jako C, C++ nebo Java zvládne rychleji než Python, řada příkladů ilustrujících to, jak je Python pomalý, je poměrně hloupá a plyne z nepochopení toho, jak Python funguje. Ovšem i řada zastánců se dopouští při obhajobě rychlosti Pythonu podivných přešlapů a demostruje to spravení špatně napsaného kódu, který následně v Pythonu trochu opraví způsobem, který je aplikovatelný v každém jazyce (např. cachování při výpočtu Fibonacciho čísel). Zkrátka bad code is bad. Během tohoto kurzu se pokusím osvětlit, co stojí za tím, že je Python pomalý a jak se tomu vyhnout jiným způdobem, než úpravou algoritmu.

Druhá výtka nesměřuje ani tak na Python samotný, jako spíš celkově na dynamicky typované jazyky. Mnoho lidí považuje dynamické typování za zdroj veškerého zla ve světě. Má zkušenost ukazuje, že záleží na kontextu. V kratších či jednodušších programech nebo prototypech umožňuje dynamické typování psát rychleji a v jistém smyslu přehledněji. Kód v dynamicky typovaném jazyce může být poměrně univerzální (viz například funkce average výše - bude fungovat s celými i necelými čísly). Ale u větších projektů to bývá právě tato nevázanost, která se vymstí. Zhruba od verze 3.6 se tedy v Pythonu začíná objevovat podpora typování, určená ke statické analýze kódu. Je to nenahraditelný pomocník, který pomůže odhalit řadu spících chyb dříve než v provozu. Budeme se tomu věnovat.

Interpretovaný, nikoliv kompilovaný. Možná trochu obojí.#

Jednoduchá ne tak úplně pravda: Python je interpretovaný jazyk. Abychom mohli program napsaný v jazyce Python spustit, potřebujeme mít nainstalový program, kterému se říká interpret, který napsaný program řádek po řádku prochází a vykonává. Liší se tak od kompilovaných jazyků, jako například C, C++, Fortran. Programy napsané v nich překládá takzvaný kompilátor do strojové jazyka, který se už skládá z instrukcí přímo pro procesor.

Složitější víceméně pravda: Python ve skutečnosti prochází kompilací a je to až výsledek této kompilace, který je interpretován. Ten zásadní rozdíl oproti kompilovaným jazykům je, že výsledek této prekompilace není strojový kód, ale v případě pythonu tzv. bytecode, což je jakási low-level reprezentace jazyka, kterou pak Python Virtual Machine (PVM) přímo vykonává. Právě PVM zde pak hraje roli toho interpreta.

Tento přístup je poměrně běžný (stejně to dělá např. Java). Má to své výhody - kompilátor typicky provede pár věcí navíc, jako např.:

  • řadu kontrol (v této fázi např. hlásí SyntaxError, se kterou se v průběhu semestru dobře seznámíte),

  • základní sémantickou analýzu,

  • mírnou optimalizaci (oproti např. jazyku C++ skutečně mírnou),

  • vygeneruje bytecode.

Pokud se zdrojový kód nezměnil, bytecode se znovu nevytvaří a jeho vykonávání bývá typicky rychlejší než přímá interpretace. Prekompilované kódy naleznete ve složce __pycache__.

Je dokonce možné Python zcela zkompilovat do jednoho standalone executable souboru, ale v praxi to znamená, že součástí toho executable musí být is celý PVM. Je to relativně obtížné a přináší to minimum výhod. Nebudeme se tím zabývat.

Note

Pro rýpaly: ano, správně, jazyk není interpretovaný ani kompilovaný - to jsou vlastnosti jeho implementace (o nich více níže). Když se budete nudit, můžete napsat kompilátor pro Python nebo interpret pro C, ale bude to těžké. Některé implementace jazyka Python (PyPy) zahrnují just-in-time (JIT) kompilaci, která bytecode překládá přímo do strojového.

Co tedy vlastně Python je?#

Jenoduše řečeno, Python je programovací jazyk. Jeho sémantiku a syntaxi naleznete popsanou v dokumentaci v sekci The Python Language Reference a základní výbavu, tedy moduly a balíčky, kterou jsou součástí, v sekci The Python Standard Library. V zásadě vám nic nebrání toto vše nějakým způsobem implentovat a výslednému produktu můžete říkat Python. K tomuto datu (únor 2024) si nejsem vědom žádné formálnější specifikace a jsem přesvědčen, že neexistuje. Ale sami autoři přiznávají, že Language Reference není jednoznačně specifikující:

…if you were coming from Mars and tried to re-implement Python from this document alone, you might have to guess things and in fact you would probably end up implementing quite a different language.

Není překvapivé, že existuje několik implementací jazyka Python. Mezi hlavní patří

  • CPython: nejpopulárnější a nejrozšířenější implementace jazyka, napsaná v jazyce C. Nainstalujete-li si Python naslepo, velmi pravděpodobně si nainstalujete tuto implementace. Projekt je open-source, můžete se na něm klidně podílet. Doporučuji do zdrojových kódů CPython občas nahlédnout, může to být velmi poučné. Vše ke stažení na GitHubu.

  • Jython: implementace v Java.

  • PyPy: pozoruhodná, ale populární implementace - Python implementovaný v jazyce Python. Mírně rozšiřuje základní výbavu, dodává JIT, čímž v některých případech dosahuje až pětinásobného zrychlení.

V průběhu semestru budu mluvit v podstate výhradně o CPython. Pokud budete používat jinou implementaci, můžete narazit na rozdíly a nemohu garantovat, že vám vše bude fungovat.

PEP#

Vývoj Pythonu je řízen prostřednictvím takzvaných PEPs - Python Enhancement Proposals. V nich nalezneme dokumenty popisující širokou škálu záležitostí - postup při návrhu nových funkcí, postup při nakládání s deprecated vlastnostmi nebo třeba samotné návrhy nových vlastností. Vyberme z nich dva nejznámější příspěvky.

PEP 8#

Style Guide for Python Code. V průběhu semestru se budeme snažit tímto řídit (ikdyž přímo v PEP 8 se píše, že vynucovat si styl za každou cenu je pošetilé

“A Foolish Consistency is the Hobgoblin of Little Minds”).

PEP 20#

The Zen of Python. Má jít od seznam 20 principů, jimiž se návrh Pythonu řídí, ale dostalo se jich tam jen 19.

  1. Beautiful is better than ugly.

  2. Explicit is better than implicit.

  3. Simple is better than complex.

  4. Complex is better than complicated.

  5. Flat is better than nested.

  6. Sparse is better than dense.

  7. Readability counts.

  8. Special cases aren’t special enough to break the rules.

  9. Although practicality beats purity.

  10. Errors should never pass silently.

  11. Unless explicitly silenced.

  12. In the face of ambiguity, refuse the temptation to guess.

  13. There should be one– and preferably only one –obvious way to do it.

  14. Although that way may not be obvious at first unless you’re Dutch.

  15. Now is better than never.

  16. Although never is often better than right now.

  17. If the implementation is hard to explain, it’s a bad idea.

  18. If the implementation is easy to explain, it may be a good idea.

  19. Namespaces are one honking great idea – let’s do more of those!

Kde a co studovat?#

Warning

Buďte opatrní při používání náhodných YT tutoriálů nebo online návodů - často jsou v nich nesmysly. Když si nejste jisti, můžete se mě zeptat.

CLI#

Pro práci s Pythonem se hodí umět ovládat příkazovou řádkou (command prompt, terminal). Do příkazové řádky zadáváme příkazy, kterými můžeme manipulovat se systémem, souboru atd. Nějakou formu příkazové řádky najdeme v každém běžném systému. Ačkoliv každý systém a každá příkazová řádka má svá specifika, za kládní princip a manipulace jsou stejné nebo jen mírně odlišné. Následující odstavce shrnují to nejnutnější, co budeme potřebovat, jak pro systém Windows, tak pro Linux a MacOS (u nich je vše praktické stejné).

Ve Windows ji najdeme jako program s názvem Command Prompt, a snadno jej spustít např. pomocí Win+R, což otevře konzoli “Run”, kam zadáte cmd a potvrdíte.

Na MacOS stačí vyhledat aplikaci Terminal.

Pokud používáte linux, příkazovou řádku otevřít umíte.

Rozhraní příkazové řádky je jednoduché. Typicky zobrazuje, v jaké složce uživatel nachází, což po startu bývá jeho domovský adresář, a očekává od něho vstup - příkaz (command prompt). Na Windows to vypadá takto

C:\Users\Vaclav> 

Na Linux a na MacOS nějak takto:

vaclav@mypc:~$ 

kde vlnovka ~ je zkratka právě pro domovský aresář /home/vaclav.

Následující tabulka shrnuje základní příkazy.

Windows

mac/linux

aktuální složka

cd

pwd

vypsat soubory a složky

dir

ls

přejít do složky s cestou path

cd path

cd path

vytvořit složku newdir

mkdir newdir

mkdir newdir

odstranit soubor file

del file

rm file

odstranit slozku dir

rmdir dir

rmdir dir

Příkazy, programy a PATH#

Některé příkazy umít vykonávat přímo program reprezentovaný příkazovou řádkou (konkrétní command processor). Většina příkazů jsou ve skutečnosti samostatné programy, které ten či onen command processor umí “najít”. Systém (případně konkrétní command processor - na linux třeba bash) si udržuje seznam míst, kde se nachází programy, které mají být dostupné jako příkazy. Tento seznam míst se nachází právě v proměnné prostředí PATH.

Jedním z takových příkazů-programů je například pythoní interpret, tedy program s názvem python (na Linuxu a často na MacOS python3), díky kterému můžeme spouštět programy napsané v Pythonu

C:\User\vaclav\programovani> python hello.py
Hello World!

Warning

Abychom nemuseli na Windows ručně upravovat proměnnou PATH, je třeba při instalaci Pythonu zaškrtnout možnost “Add Python to PATH”. Systém by jinak nevěděl, kde se interpret Pythonu nachází a neuměl by program spustit.

Pracovní prostředí#

Doporučuji si pro potřeby tohoto kurzy založit složku, do které si budete vkládat vše, co zde budeme dělat. Je jedno, kde to bude. V knize budu v pracovat ve složce python umístěné v domovském adresáři, tedy pro linux/mac:

vaclav@mypc:~/python$ 

nebo pro Windows

C:\Users\vaclav\python> 

A protože ta samotná složka většinu času nebude příliš důležitá, a většina pro nás důležitých příkazů je stejná či podobná na Windows/Linux/Mac, budu většinu konzolových vstupů redukovat na toto

$ 

Bude-li nutné zvlášť vyčlenit příklad pro Windows, uvedu

>

Python console#

Občas budu v textu ukazovat příkazy v Pythoní konzoli (o ní ještě bude řeč) a budu ji značit >>>, tj.

>>> 

Instalace#

Instalací jazyka Python máme na mysli instalaci interpreta. V zásadě jsou na výběr dvě možnosti:

  1. standardní Python, python.org

  2. Anaconda distribution, anaconda.com

Anaconda je distribuce Python, která sdružuje balíčky relevantní pro “data science” a součástí je i manažer balíčku (alternativa k pip). Na Windows a MacOS funguje příjemně, na Linux je třeba postupovat trochu opatrněji kvůli případným kolizím se systémovou instalací.

Celé materiály budou psané s ohledem na standardní Python, jehož instalace je popsána v knize k předmětu - zde.

Instalaci ověříme tak, že zkusíme z příkazové řádky (konzole, terminálu, command line atp.) interpreta spustit.

> python   # win
$ python3  # linux/macos

Tip

Pokud uvidíte nějakou zprávu o tom, že příkaz python je neznámý, znamená to, že instalace selhala, nebo že python nebyl přidán do proměnné PATH. Zkuste interpreta přeinstalovat nebo si vyhledejte, jak se PATH aktualizuje manuálně.

První program#

V libovolném textovém editoru si připravíme textový soubor hello.py, do kterého umístíme

print("Hello World!")

a uložíme do naší pracovní složky. V konzoli pak program spustíme tak, že interpretu dáme cestu k souboru

# windows
> python hello.py
Hello World!

# linux/mac
$ python3 hello.py
Hellow World!

Balíčky a virtuální prostředí#

Správa balíčků v Pythonu prošla divokým vývojem a existuje spousta navzájem špatně kompatibilních možností, jak trefně ilustruje následující příspěvěk z xkcd. XKCD: Python environment

V poslední době se situace ustaluje a většinu času v nepříliš specifických projektech vystačíme se standardním manažerem balíčků pip, případně s Anacondou.

Virtuální prostředí#

V praxi se může stát, že balíčky/moduly, které potřebujeme pro práci na různých projektech, jsou navzájem nekompatibilní, nebo si prostě chceme nějaký balíček vyzkoušet, aniž by zasahoval do funkčního prostředí.

K tomu slouží modul virtualenv, pomocí kterého můžeme vytvářet nezávislá prostředí, do nichž je možné instalovat další balíčky/moduly nezávisle. Modul virtualenv je nutné nainstalovat globálně, tedy v příkazové řádce:

pip install virtualenv   # win
pip3 install virtualenv  # linux/macos

Virtuální prostředí vytvoříme příkazem

virtualenv <my_env_name>

nebo

python -m virtualenv <my_env_name>   # win
python3 -m virtualenv <my_env_name>  # linux/mac

který v aktuální složce vytvoří novou složku s názvem <my_env_name>.

Virtuální prostředí je nutné aktivovat:

path\to\my_env\Scripts\activate    # win
source path/to/my_env/bin/activate # linux/macos

Po úspěšné aktivace se na začátku každého řádku v konzoli objeví název prostředí v závorce. Je-li prostředí aktivní, veškerá činnost související s pythonem nadále probíhá v tomto prostředí. Včetně instalace nových balíčků.

Important

Pokud si nejste jistí, co děláte, raději nikdy neinstalujte balíčky mimo virtuální prostředí.

Warning

Platnost/aktivita virtuálního prostředí je omezena na konkrétní okno příkazové řádky. Otevřu-li novou instanci příkazové řádky, žádné virtuální prostředí v ní aktivní nebude (bude chybět název v závorce).

Práci s virtuálním prostředím mohu ukončit jeho deaktivací příkazem

deactivate                        # linux/macos (nekdy i win)
path\to\my_env\Scripts\deactivate # win (vetsinou)

Pokud se ve virtuálním prostředí něco pokazí, například:

  • instalace nového balíčku

  • balíčky po aktualizaci přestanou být kompatibilní

  • balíček obsahuje závažné chyby působící kolaps interpreta mohu virtuální prostředí jednoduše smazat (smazaním prostředí obsahující složky) a systémová instalace zůstane nedotčena.

V čem psát Python#

Textový editor#

Na jednoduché skripty nám obvykle stačí obyčejný textový editor. Program napsaný v Pythonu je stejně textový soubor, který spouštíme tak, že jej předhodíme interpretovi.

Konzole#

Obdobou k systémové konzoli jsou konzole zaměřené na konkrétní jazyk. Samotný Pythoní interpret. Tedy například:

$ python3
Python 3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Konzole je vhodná na drobné úkony, nebo testy, či jako schopnější kalkulačka. Na jakékoliv programování je spíše nevhodná.

Existuje i několik variací pokročilejších konzolí, uveďme příkladem nástroj ipython. Oproti výchozí pythoní konzoli ipython nabízí například automatické doplňování, nápovědu atd. Snadno ho nainstalujeme pomocí

# linux/mac
$ pip3 install ipython
# windows
> pip install ipython

Součástí instalace Pythonu (na Windows pravděpodobně, na Linux někdy, na MacOS nevím) je i jednoduché Pythoní skoro-IDE s názvem Idle (po Ericu Idleovi ze skupiny Monty Pyton’s Flying Circus).

Osobní názor

Osobně jsem nikdy nepochopil, k čemu Idle je. Ovládá se špatně, nic moc neumí a navíc je ohyzdný.

IDE#

Na trhu je v současné době několik IDE (integrated development environment).

  • Spyder solidní OpenSource IDE.

  • PyCharm z dílny firmy JetBrains je plnohodnotné IDE se spoustou integrovaných funkcí (linting, code inspection, refactoring tools, napojení na VCS, solidní debugger atd.). Community edition je zdarma (na většinu běžné práce stačí), jako studenti máte nárok na plnou licenci (alespoň myslím).

  • VS Code čím dál populárnější nástroj od Microsoftu. Spousta funkcionality stojí na pluginech. To znamená, že může VS Code použít takřka na cokoliv, ale rizikem je, že pluginy budou nějakým způsobem nekompatibilní.

  • Visual Studio prý to je použitelné. Nesetkal jsem se s funkčním pokusem.

Hybrid#

Kombinace živé konzole s prvky IDE a možností ukládání session. Na tomto poli dominují nástroje ze sady Jupyter, které se vyvinuly z původního ipython. Nástroj, který je skvělý na seznámení s jazykem, prototypování a či některá odvětví aplikované data science.

Osobní názor

Na rozsáhlejší projekty je to prostředí krajně nevhodné. Nepořádek, který vzniká v globálním scope vytváří velmi nezdravé programovací návyky.

Jupyter-notebook, či jeho bohatší alternativu Jupyter-lab budeme používat na hodinách. Nástroj není povinný, ale formou jupyter notebook budu sdílet většinu poznámek z hodin.

Jupyter#

JupyterLab is the latest web-based interactive development environment for notebooks, code, and data. Its flexible interface allows users to configure and arrange workflows in data science, scientific computing, computational journalism, and machine learning. A modular design invites extensions to expand and enrich functionality.

Projekt Jupyter přináší dva produkty Jupyter Notebook a Jupyter Lab. Základem je koncept Notebooku - sešitu, který se skládá z buněk dvou typů: python a markdown[1]. Je to formát, který je populární pro výukové materiály, prezentaci výsledků či jednoduché interaktivní aplikace. Jupyter Lab k tomu přidává i poněkud bohatší prostředí, přinášející i několik prvků klasických IDE.

Celé to má podobu webové aplikace. Spustit Jupyter Notebook/Lab tedy typicky znamená spustit lokální webový server, který vše obsluhuje.

Ukázky, návody apod. naleznete na domovské stránce projektu: Jupyter. Zde si pouze ukážeme, jak se Jupyter instaluje.

Jupyter a první program#

Budu zde vycházet z pracovního prostředí, tak jak je popsáno zde. Níže je celý postup pro Windows a pro Linux/MacOS, včetně přípravy virtuálního prostředí. Můžete samozřejmě využít již existující virtuální prostředí.

Important

Stejně jako ostatní balíčky, i Jupyter instalujeme zásadně do virtuálního prostředí.

Windows#
> cd
C:\Users\vaclav\python
# vytvoreni virt. prostredi se standardnim nazvem venv
> virtualenv venv
# muzeme overit vypsanim slozek, venv bude mezi nimi
> dir
# aktivace prostredi
> venv\Scripts\activate
# aktivni prostredi je uvede v zavorce na zacatku promptu
(venv) >
# instalace Jupyteru
(venv) > pip install notebook
# spusteni prostredi jupyter notebook
(venv) C:\python>jupyter-notebook
Linux/MacOS#
$ pwd
/home/vaclav/python
# vytvoreni virt. prostredi se standardnim nazvem venv
$ virtualenv venv
# aktivace prostredi
$ source venv/env01/bin/activate
(venv) $ 
# instalace Jupyteru (pokud jeste nemame)
(venv) $ pip3 install notebook
# spusteni prostredi
(venv) $ jupyter-notebook

Note

Prostředí Jupyter Lab se instaluje pod názvem jupyter-lab, ale spouští se pod názvem jupyterlab. Ty pomlček v průběhu let přicházely a odcházely. Nebude-li vám to fungovat, vyzkoušejte více kombinací.

První program v Jupyteru#

V prostředí Jupyter-notebook tak uvidíte všechny soubory a složky, které se nachází v naší kořenové složce python, ze které jsme Jupyter spustili.

Vyvořte si v prostředí novou buňku, ujistěte se, že je typu Code a vepiště do ní

print("Hello world")

Klepnutím na tlačíko “play” či stiskem kláves Ctrl+Enter buňku spustíte.


Virtual environment jako Jupyter kernel#

Situace 1: Chceme virtuální prostředí, v němž je nainstalovaný Jupyter, nazvěme ho jupyter-env. V tomto prostředí chceme mít v Jupyter notebook k dispozici jiné prostředí jako kernel, nazvěme ho myenv.

  1. V prostředí myenv musí být nainstalován modul ipykernel.

source /path/to/my/virtualenvs/myenv/bin/activate
pip3 install ipykernel
deactivate
  1. Přidáme prostředí myenv do Jupyter kernels v prostředí jupyter-env

/path/to/myenv/bin/python3 -m ipykernel --prefix=/path/to/jupyter-env \
--name myenv-name --display-name "My virtual environment"

Tento příkaz vytvoří v /path/to/jupyter-env/share/jupyter/kernels složku myenv-name, ve které se nachází soubor kernel.json obsahující informace pro Jupyter, kde a jak spustit kernel myenv, neboli jak zpracovat skript v Jupyter notebook v rámci prostředí myenv. Obsah kernel.json je následující

{
 "argv": [
  "/path/to/myenv/bin/python3",
  "-m",
  "ipykernel_launcher",
  "-f",
  "{connection_file}"
 ],
 "display_name": "My virtual environment",
 "name": "myenv-name"
}

Parametr display_name je název, pod kterým bude prostředí/kernel myenv k dispozici v Jupyter notebook, name je jméno pro interní potřeby Jupyteru. argv jsou command line argumenty, kterými Jupyter kernel přímo spouští, tedy volá příkaz

/path/to/myenv/bin/python3 -m ipykernel_launcher -f {connection_file}

Odsud je vidět, proč musí být modul ipykernel nainstalovaný i v prostředí myenv.

Situace 2: Máme globálně nainstalovaný Jupyter a chtěli bychom v něm mít k dispozici virtuální prostředí myenv jakožto kernel.

  1. V prostředí myenv musí být nainstalován modul ipykernel

source /path/to/my/virtualenvs/myenv/bin/activate
pip3 install ipykernel
deactivate
  1. Přidáme prostředí myenv do Jupyter kernels

/path/to/myenv/bin/python3 -m ipykernel --user \
--name myenv-name --display-name "My virtual environment"

Tento příkaz vytvoří složku myenv-name v .local/share/jupyter/kernels, která opět obsahuje soubor kernel.json. Volba --user zde zajišťuje zápis do ~/.local/..., tedy per-user nikoli system-wide instalaci kernelu. Bez volby --user se kernel nainstaluje do /usr/share/jupyter/kernels. Pak bude kernel přístupný pro každého uživatele, ale instalace bude vyžadovat root oprávnění.

Poznámka: Můžeme nainstalovat kernel i zevnitř aktivního prostředí myenv. Pro situaci 1:

source /path/to/my/virtualenvs/myenv/bin/activate
pip3 install ipykernel
python3 -m ipykernel --prefix=/path/to/jupyter-env \
--name myenv-name --display-name "My virtual environment"

A pro situaci 2:

source /path/to/my/virtualenvs/myenv/bin/activate
pip3 install ipykernel
python3 -m ipykernel --user --name myenv-name --display-name "My virtual environment"

Užitečné odkazy#

Datové typy, datový model a správa paměti#

V této části si popíšeme, co se v jazyce Python vlastně ukrývá za proměnnými, jak rozpoznává datové typy a jakým způsobem nakládá s pamětí.

Datové typy#

V této části si popíšeme, co se v jazyce Python vlastně ukrývá za proměnnými, jak rozpoznává datové typy a jakým způsobem nakládá s pamětí.

Python data structure Obrázek z webu GeeksForGeeks: https://www.geeksforgeeks.org/python-data-types/

a = 1024
print(a, type(a), id(a)) # tiskne value, type a id
b = 1025
print(b, type(b), id(b))
1024 <class 'int'> 140658298541904
1025 <class 'int'> 140658298540880
b = 765.12
print(b, type(b), id(b))
765.12 <class 'float'> 140658299004272

Všechny tyto vlastnosti můžeme i porovnávat. Operátor == porovnává hodnot, zatímco operátor isporovnává právě id. Rozlišujeme tím tak objekty s totožnou hodnotou a objekty identické.

a = 1000
b = 1000
print(a == b, a is b)
True False
a = 1000
b = a
print(a == b, a is b)
True True

Implicitní a explicitní konverze#

Mezi jednotlivými datovými typy může docházet ke konverzi. Pokud konverzi provede sám Python, protože je např. nutná k dokončení operace, hovoříme o implicitní konverzi.

a = 2
b = 3.0
c = a + b
print(type(a), type(b), type(c))
<class 'int'> <class 'float'> <class 'float'>
c = 1 / 2
print(type(c))
<class 'float'>

Pokud konverzi vynutím e sami, pak se jedná o explicitní koverzi. Konverzi na daný typ explicitně vynucujeme voláním konstruktoru finálního typu, tj. funkcí se stejným názvem.

a = "1"
b = float(a)
c = int(a)
d = bool(a)

a, b, c, d
('1', 1.0, 1, True)

Je nutno dávat pozor, zda konverze skutečně vede k výsledku, který očekáváme - vždycky lepší vyzkoušet. Moje oblíbená chyba:

bool_flag = bool("t")
print(bool_flag)
bool_flag = bool("f")
print(bool_flag)
True
True

Warning

Interpret si nemusí vždycky myslet to samé co vy.

Číselné typy a reprezentace čísel v počítači#

Python nabízí tři základní číselné datové typy:

  • int - integer reprezentující celá čísla,

  • float - čísla s plovoucí desetinnou čárkou,

  • complex - reprezentující komplexní čísla (fakticky dvakrát float)

Konceptuálně prosté, ale je dobré se podívat na pár vlastností souvisejících s reprezentací čísel v počítači. I v tomto má Python jistá specifika.

Celá čísla#

Celá čísla jsou v počítači reprezentována prostě - jedná se o přímočarý zápis ve dvojkové soustavě. Standardní int má běžně 32 bitů (4 byty), z nichž první je rezervován pro indikaci znaménka. Ukažme si, jak to funguje na typu char (to samé, jen s jedním bytem).

char a = 127;
char b = a + 1;
printf("%d", b);

Kolik je 127+1? Správně, -128. Proč? Záporná čísla, tj. čísla s 1 v první bitu, se interpretují pomocí dodatečné operace bitwise NOT+1. Tedy prohodíme 0 a 1 a k výsledku přičteme číslo jedna. Ukažme si pár čísel v rozsahu jednoho byte a pochopíme.

base 10

base 2

NOT

two’s complement

1

00000001

10

00001010

16

00010000

127

01111111

-127

10000001

01111110

01111111

-1

11111111

00000000

00000001

-2

11111110

00000001

00000010

-6

11111010

00000101

00000110

Odsud už je vidět proč 127+1=-128:

127=011111111
127+1=10000000
NOT(10000000)=01111111
NOT(10000000)+1=10000000
10000000=128
-> -128

Note

Jedná se o způsob reprezentace zvaný Two’s complement. Detaily naleznete třeba na Wiki Two's complement [1]

Pro 32 bitový signed int obdobně zjistíte, že 2147483647+1=-2147483648. Chybám tohoto druhu se říká Overflow Error a mohou být velmi zrádné. Stály za řadou různě závažných nehod, uveďme si dvě

  1. ESA Ariane flight V88 - přetekla hodnota horizontal bias ukládáná do 16-bit signed integer proměnné. Následná neošetřená hardware exception zablokovala část řízení a raketa se výrazně odchýlila od planované trasy. Let ukončil autodestrukční systém (Ariane flight V88 [2]).

  2. V roce 2012 vydala korejská hudební skupina Psy svůj velkolepý hit Gangnam Style. A již v roce 2013 překročil počet shlédnutí na YouTube 2 147 483 647, čímž se dostal do záporných hodnot. Google tenkrát přešel od 32-bit signed integer pro reprezentaci počtu shlédnutí k 64-bit signed integer. Doposud se Psy počet 9 223 372 036 854 775 807 shlédnutí překonat nepodařilo. (How Gangnam Style broke YouTube [3])

Proč o tom hovoříme? Protože Python je implementovaný v C, tak bychom mohli u typu int očekávam nějakou podobnou záludnost.

Ve skutečnosti Python používá C typ long, což je 8 bytová reprezentace celého čísla. Pokud připustíme unsigned variantu (tedy pouze kladná čísla, i první bit se použije), je maximální hodnota 2^64 - 1 = 18 446 744 073 709 551 615. Z toho, co jsme viděli víme, že navýšíme-li toto číslo o 1, dostaneme 0. Zkusme to v pythonu.

a = 18_446_744_073_709_551_615
b = a + 1
print(a)
print(b)
18446744073709551615
18446744073709551616

Dostali jsme správný výsledek. Už samotný int objekt je poměrně složitý a dokáže pracovat se složitější reprezentací celých čísel a v případě potřeby alokuje více paměti. Int v pythonu je neomezený.

Floating point numbers#

Následující (ne)rovnost naplňuje řadu lidí bázní, mnohým se hroutí svět a jiní v návalu paniky začínají obviňovat konkrétní programovací jazyky.

0.1 + 0.2 == 0.3
False

My budeme chytřejší a seznámíme se s tím, proč si i Váš počítač myslí, že

0.1+0.2
0.30000000000000004

Následující kód definuje několik pomocných funkcí, které zobrazují binární reprezentaci floating point čísel v počítači. Jejich implementace nás nemusí zajímat, věnujme se těm číslům. Z porovnání je zřejmé, že výsledky skutečně nejsou totožné.

import struct
def binary(num):
    return ''.join('{:0>8b}'.format(c) for c in struct.pack('!d', num))

def print_by_sections(num: str):
    print(num[0], num[1:12], num[12:16], "", end="")
    for i  in range(6):
        print(num[16+i*8:16+(i+1)*8], "", end="")
    print()
    
def show_repr(num):
    print_by_sections(binary(num))

show_repr(0.1)
show_repr(0.2)
show_repr(0.1+ 0.2)
show_repr(0.3)
show_repr(0.1+0.2-0.3)
0 01111111011 1001 10011001 10011001 10011001 10011001 10011001 10011010 
0 01111111100 1001 10011001 10011001 10011001 10011001 10011001 10011010 
0 01111111101 0011 00110011 00110011 00110011 00110011 00110011 00110100 
0 01111111101 0011 00110011 00110011 00110011 00110011 00110011 00110011 
0 01111001001 0000 00000000 00000000 00000000 00000000 00000000 00000000 

Reprezentace necelých čísel v počítači je výrazně složitější. Python používá variantu definovanou standardem IEE 754, která je vlastně binární obdobou toho, čemu říkáme scientific notation. Podle počtu použitých bitů rozlišujeme single (32 bitů) a double (64 bitů) precision floating point number.

Wiki Wiki

Výhodou této reprezentace je, že umožňuje uchovávat čísla v obrovském rozsahu hodnot: zhruba \(10^{-38}\)\(10^{38}\) pro single precision, \(10^{-308}\)\(10^{308}\) pro double precision. Python pro svůj typ float ve skutečnosti používá double precision.

Protože v double precision má mantisa (significand, tj. to, co není exponent) 52 bitů, dochází k ořezu. Obecně v single precision můžeme očekávat 7-8 platných číslic v desítkové soustave, v double precision 15-16. Některá čísla proste budou reprezentována s chybou. Tyto chyby se při operacích můžou kumulovat a je tak možné zdánlivě nevinným výpočtem dojít k úplně špatnému výsledku. Tomu, jak se takovým problémům vyhnout, se věnuje numerická matematika.

My si z toho vezmeme prozatím jednoduché ponaučení - nikdy nebudeme porovnávat float přímo, ale budeme se dívat, jestli jsou dostatečně blízko. Tedy něco jako

import math

a = 0.1 + 0.2
b = 0.3

if a == b:
    print("this will not work")

if abs(a-b) < 1e-15:
    print("this should work for small numbers")

if abs((a-b)/a) < 1e-15:
    print("this should work for large numbers")

if math.isclose(a, b, rel_tol=1e-15):
    print("this should be always safe")
this should work for small numbers
this should work for large numbers
this should be always safe

Danger

POČÍTAČ NEUMÍ POČÍTAT! Ale dělá to velmi rychle. Počítejme s tím.

Bool#

Na rozdíl od řady programovacích jazyků je v Pythonu samostatný logický typ bool. Může nabývat dvou různých hodnot:

True
False

Hodnoty typu bool jsou typicky výstupem z vyhodnocení logických výrazů, jako je např. porovnávání.

Kolekce#

Kontejnery (někdy kolekce) jsou takové datové typy, které mohou obsahovat další prvky. Možným kritériem pro jejich další dělení je uspořádání.

  • Sekvence (řazené) – prvky mají definované pořadí, přistupujeme k nim pomocí indexů v hranaté závorce. Patří sem např. list, tuple nebo string.

  • Neřazené – prvky nemají žádné konkrétní pořadí, indexování tady nemá smysl. Patří sem např. set.

Jiným příkladem kontejneru je dict, neboli dictionary, slovník – mapovací typ. Od verze Python 3.6 má definované pořadí, ale indexování jako sekvence nepodporuje.

List#

list (seznam) je druh mutable (měnitelné) sekvence, která se typicky používá jako obdoba dynamických polí.

Listy mohou obsahovat prvky různých typů. Zapisují se hranatou závorkou. Pomocí zabudovaných metod, jako .append(), nebo .remove() můžeme obsah listu měnit.

>>> my_list = [1, 2.71, "jogurt", True]
>>> my_list[0] = 2
>>> my_list.append(False)
>>> my_list.remove("jogurt")
>>> my_list
[2, 2.71, True, False]
Tuple#

tuple (n-tice) je immutable (neměnitelná) sekvence, která se typicky používá jako obdoba statických polí.

Tuples mohou obsahovat prvky různých typů. Zapisují se kulatou závorkou. Obsah tuple můžeme číst, ale nemůžeme měnit.

>>> my_tuple = (1, 2.71, "jogurt", True)
>>> my_list[2]
jogurt
>>> my_tuple[0] = 2  # -> TypeError
Dictionary#

Dictionary (slovník) je mutable (měnitelný) mapping, tedy typ, který obsahuje dvojice key a value (klíč a hodnota).

Slovníky mohou obsahovat prvky různých typů a mnoho typů lze i použít jako klíč. Zapisují se složenou závorkou, klíč a hodnotu odděluje dvojtečka. K hodnotám ve slovíku přistupujeme pomocí odpovídajících klíčů.

String#

String str (řetězec) je neměnitelná (immutable) sekvence znaků. Jakožto sekvence podporuje standardní indexování včetně sliců. Přestože je kolekcí, zaslouží se vlastní sekci.

Zapisuje se pomocí jednoduchých i dvojitých uvozovek.

>>> my_str = 'jogurt'
>>> my_str[3]
'u'
>>> my_str + 'y'
'jogurty'
>>> my_str[0] = "J"
>>> my_str
"Jogurty"
>>>
>>> my_str = "Příliš žluťoučký kůň úpěl ďábelské ódy"
>>> my_str[7:16]
'žluťoučký'
>>> my_str[::-1]
'ydó ékslebáď lěpú ňůk ýkčuoťulž šilířP'

Velikou výhodou typu string v Pythonu je, že v sobě má zabudované nepřeberné množství nejrůznějších operací.

Všechny najdete v dokumentaci. Namátkou jich pár vyberme:

>>> my_str = "Příliš žluťoučký kůň úpěl ďábelské ódy"
>>> my_str.split(" ")
['Příliš', 'žluťoučký', 'kůň', 'úpěl', 'ďábelské', 'ódy']
>>> "_".join(["hello", "world"]).upper()
'HELLO_WORLD'
>>> '###hello###'.strip('#')
'hello'

NoneType#

Významným datovým typem je NoneType, který slouží k reprezentaci “ničeho”, tedy nedefinované, nepřiřazené nebo neexistující hodnoty, anglicky null value.

Jeho hodnotou je None, funkce type navrací NoneType a na typ bool se vždy konvertuje jako False. Bežně se s ním setkáme např. jako s návratovou hodnotou funkcí, které nic nevrací.

Zda je proměnná typu NoneType, porovnáváme pomocí operátoru is

def f():
    # this function does not return anything
    pass

a = f()

if a is None:
    print("there is nothing in var a")
there is nothing in var a

Note

Operátor is lze použít, neboť NoneType je implementován jako singleton, takže všechny instance NoneType jsou identické objekty. Vzpomeňte si na kurzy návrhových vzorů, které Vás možná teprve čekají.

Formátovaný výstup#

Velmi často potřebujeme obsah proměnných nějakým způsobem vytisknout či reprezentovat v řetězci (str). Tomuto procesu se obvykle říká string interpolation a Wiki jej definuje takto

In computer programming, string interpolation (or variable interpolation, variable substitution, or variable expansion) is the process of evaluating a string literal containing one or more placeholders, yielding a result in which the placeholders are replaced with their corresponding values. It is a form of simple template processing[1] or, in formal terms, a form of quasi-quotation (or logic substitution interpretation). The placeholder may be a variable name, or in some languages an arbitrary expression, in either case evaluated in the current context.

Alternativou ke string interpolation je string concatenation, tedy spojování řetězců, ale tomu se věnovat nebudeme (neboť je krkolomné a nepraktické).

Python aktuálně nabízí celkem 4 možnosti interpolace, z nichž některé jsou považovány za zastaralé a měli bychom se jim vyhýbat:

  1. Formatted string literals (často nazýváno f-string),

  2. Metoda str.format,

  3. Template strings,

  4. printf style (někdy C style) print.

Web Real Python uvádí celkem rozumný diagram, jak zvolit správnou variantu (Python String Formatting Best Practices [4])

Všimněte si, že printf style formatting v diagramu zcela chybí - ten už by se neměl používat vůbec. Podíváme se ale na všechny.

Formatted string literals - f-strings#

Novinka od verze 3.6 a nyní preferovaná varianta. V řetězcích s prefixem f můžeme používat ve složených závorkách názvy proměnných (definované symboly)

# https://www.ncbi.nlm.nih.gov/pmc/articles/PMC5052503/
low = 0.5
high = 1.5
info = f"The average human produces between {low} and {high} litres of saliva every day."
print(info)
The average human produces between 0.5 and 1.5 litres of saliva every day.

Způsob, jakým je proměnná do řetězce vložena, lze samozřejmě důkladně kontrolovat pomocí Format Specification Mini-Language, který je důkladně popsán v dokumentaci a přináší mnoho možností zaokrouhlování, zarovnávání a podobně. Zde jen pár příkladů:

pi = 3.14159265
formatted_pi = f"{pi:.2f}"
print(formatted_pi)
3.14
name = "John"
formatted_name = f"{name:=^10}"
print(f"'{formatted_name}'")
'===John==='
big_number = 1000000
formatted_number = f"{big_number:,}"
print(formatted_number)
1,000,000
accuracy = 0.985
formatted_accuracy = f"{accuracy:.2%}"
print(formatted_accuracy)
98.50%
x = 2
print(f"{x:.=20b}\n{2*x:.=20b}")
..................10
.................100
number = 255
formatted_bin = f"{number:#b}"
print(formatted_bin)
0b11111111
width = 10
precision = 4
number = 3.14159265
formatted_dynamic = f"{number:{width}.{precision}}"
print(formatted_dynamic)
     3.142

Metoda str.format#

Metoda .format pochází už z verze 2.6. Spočívá v tom, že do stringu umístíme zástupné symboly (placeholders) {}, které pak nahradím voláním právě metody .format. Je to praktické zejména ve spojení se slovníkem. I zde funguje výše zmíněný Format Specification Mini-Language. Opět pár příkladů

pi = 3.14159265
formatted_pi = "{:.2f}".format(pi)
print(formatted_pi)
3.14
name = "John"
formatted_name = "{:=^10}".format(name)
print(f"'{formatted_name}'")
'===John==='
number = 255
formatted_bin = "{:#b}".format(number)
print(formatted_bin)
0b11111111
width = 10
precision = 4
number = 3.14159265
formatted_dynamic = "{:{width}.{precision}}".format(number, width=width, precision=precision)
print(formatted_dynamic)
     3.142
data = {'name': 'John', 'age': 30, 'city': 'New York'}

formatted_string = "Name: {name}, Age: {age}, City: {city}".format(**data)
print(formatted_string)
Name: John, Age: 30, City: New York

String templates#

Bezpečnější alternativa k předchozím dvěma možnostem. Funguje také pomocí placeholderů, ale je trochu prostší z hlediska formátování. Oproti .format je odolnější proti code injection.

from string import Template

t = Template('Hello, $name!')
message = t.substitute(name='John')
print(message)
Hello, John!
from string import Template

data = {'name': 'Jane', 'occupation': 'developer'}
t = Template('Hello, $name! You are a $occupation.')
message = t.substitute(data)
print(message)
Hello, Jane! You are a developer.

Note

Nepodařilo se mi narychlo zkonstruovat jednoduchý příklad s funkční code injection. Podle všeho, co jsem zatím četl, by f-stringy měly být velmi odolné.

printf style formatting#

Jak název napovídá, tento způsob napodobuje chování funkce printf, která se k těm samým účelům používá v jazyce C. Možnosti jsou velmi omezené a i podle dokumentace bychom tento způsob již neměli používat.

Important

Tento způsob je zastaralý - NEPOUŽÍVAT.

Přesto uvedu pár příkladů užití.

name = "Alice"
age = 30
print("Name: %s, Age: %d" % (name, age))
Name: Alice, Age: 30
pi = 3.14159265
print("Pi is approximately %.2f" % pi)
Pi is approximately 3.14
number = 255
print("Hex: %x, Octal: %o" % (number, number))
Hex: ff, Octal: 377

Datový model#

Co to vlastně je proměnná v Pythonu? Jak to vypadá, co to umí, jak se to liší od ostatních jazyků?

V jazycích jako např. C, je proměnná fakticky pojmenované umístění v paměti. Příklad:

int a = 4;

Celočíselná proměnná a tady prostě ukazuje na 4 byty někde v paměti, ve kterých se nachází 0 a 1 reprezentující, v tomto případě, hodnotu 4.

Oproti tomu v Pythonu výraz

a = 4

vytváří složitější objekt, přičemž slovo objekt zde můžeme vnímat i ve smyslu OOP. Je to struktura sdružující data a nějaké operace na nich (funkce/metody) do jednoho celku. Jinými slovy, porměnná v Pythonu i něco umí.

Co všechno taková proměnná umí, tedy všechny metody objektu uloženého v dané proměnné, si můžeme vypsat použitím vestavěné funkce dir

dir(a)
['__abs__',
 '__add__',
 '__and__',
 '__bool__',
 '__ceil__',
 '__class__',
 '__delattr__',
 '__dir__',
 '__divmod__',
 '__doc__',
 '__eq__',
 '__float__',
 '__floor__',
 '__floordiv__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getnewargs__',
 '__gt__',
 '__hash__',
 '__index__',
 '__init__',
 '__init_subclass__',
 '__int__',
 '__invert__',
 '__le__',
 '__lshift__',
 '__lt__',
 '__mod__',
 '__mul__',
 '__ne__',
 '__neg__',
 '__new__',
 '__or__',
 '__pos__',
 '__pow__',
 '__radd__',
 '__rand__',
 '__rdivmod__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__rfloordiv__',
 '__rlshift__',
 '__rmod__',
 '__rmul__',
 '__ror__',
 '__round__',
 '__rpow__',
 '__rrshift__',
 '__rshift__',
 '__rsub__',
 '__rtruediv__',
 '__rxor__',
 '__setattr__',
 '__sizeof__',
 '__str__',
 '__sub__',
 '__subclasshook__',
 '__truediv__',
 '__trunc__',
 '__xor__',
 'as_integer_ratio',
 'bit_count',
 'bit_length',
 'conjugate',
 'denominator',
 'from_bytes',
 'imag',
 'numerator',
 'real',
 'to_bytes']

V Pythonu se tedy nejedná o pouhý odkaz někam do paměti, ale o poměrně komplexní objekt se spoustou funkcí. Kromě takovýchto type-specific funkcí, má každý objekt definované ještě funkce type a id a nějakou svou value. Každý objekt v Pythonu je tedy konkrétního typu, má přidělené nějaké unikátní id a má jistou hodnotu. Je dobré zdůraznit, že id objektů v Pythonu odpovídá adrese v paměti, kde se objekt nalézá.

a = 1024
print(a, type(a), id(a)) # tiskne value, type a id
1024 <class 'int'> 140253139659440
b = 765.12
print(b, type(b), id(b))
765.12 <class 'float'> 140253140138192

Všechny tyto vlastnosti můžeme i porovnávat. Operátor == porovnává hodnotu, zatímco operátor isporovnává právě id, tedy identitu objektů. Rozlišujeme tím tak objekty s totožnou hodnotou a objekty identické.

a = 1000
b = 1000
print(a == b, a is b)
True False

Proměnné a a b mají stejnou hodnotu, ale jedná se o dva rozdílné objekty; nejsou tedy identické.

a = 1000
b = a
print(a == b, a is b)
True True

Zde je proměnná b zkonstruovaná přímo z proměnné a - jedná se o identické objekty a takové mají samozřejmě stejnou hodnotu.

Interning#

Zajímavá situace ovšem nastane ve speciálních případech.

a = 150
b = 150
print(a == b, a is b)
print(id(a), id(b))
True True
140253196096400 140253196096400

Python ve skutečnosti celá čísla od -5 do 256 uchovává v bokem alokovaném poli a proměnné a a b se k nim pouze odkazují. Podobně stejné řetězce se neduplikují, ale rovněž se uchovávají bokem. Tomuto procesu se říká interning a motivací je úspora paměti. Někdy kolem verze 3.7 došlo ke změně - do té doby Python internoval řetězce do délky 40 znaků, nyní internuje až do délky 4096. Na některých strojích je dokonce vidět významně odlišné id vyčleněných hodnot oproti hodnotám ostatním. To souvisí s faktem, že idje v implementaci CPython ve skutečnosti adresa objektu v paměti.

a = "test"
b = "test"
c = "tes"
print(a == b, a is b)
print(b == c+'t', b is c+'t')
True True
True False

Mutability vs immutability#

V Pythonu podobně jako v JavaScriptu rozlišujeme mutable a immutable, tedy měnné a neměnné, datové typy. Srovnejme to opět s jazykem C. V následujícím příkladě vytváříme celočíselnou proměnnou a. Tedy někde v paměti se alokují 4 byty místa, do kterých se uloží hodnota 1. V následující řádku se do těch samých 4 bytů zapíše hodnota 2. Proměnná a stále ukazuje na ty samé 4 byty (“ukazuje” je trochu nevhodné slovo, protože to není pointer, ale to nechme být).

int a = 1;
a = 2;

Oproti tomu v Pythonu:

a = 600
print(id(a))
a = 601
print(id(a))
140253139651472
140253139653520

Po přepsání hodnoty dostáváme jiné id, tj. jinou adresu v paměti, fakticky nový objekt. To je tím, že objekt typu intje immutable, neměnný - jeho hodnotu není možné změnit. Pokud se o tom pokusíme, vzniká nový objekt. Dosavadní objekt v příkladu výše zaniká, v příkladu níže zůstává (pochopíme v části o garbage collection).

a = 400
b = a
a is b

a += 1
a is b
False

Mezi výhody immutable typů se obvykle řadí

  1. čitelnější kód (podle mě sporné1

  2. thread safety (žádné z vláken prostě immutable objekt nezmění)

  3. easier to debug (podle mě také sporné)

Osobní názor

Body 1 a 2 jsou podle mě přinejmenším sporné.

Thread safety se dá zjednodušeně ilustrovat na příkladu s funkcí:

a = 1

def f(x):
    x = 2

f(a)
print(a)
1
Které typy jsou které?#

Immutable typy

  • int

  • float

  • bool

  • string

  • complex

  • frozen set

  • tuple

  • range

Mutable typy

  • list

  • set

  • dict

Pojďme se podívat na to, jak se chová nějaký mutable typ. Ten totiž měnit můžeme, nezaniká, pouze mění svou hodnotu. Častými mutable typy jsou kontejnery.

a = [1, 2, 3]
b = a

a is b, a, b

a.append(4)
a is b, a, b
(True, [1, 2, 3, 4], [1, 2, 3, 4])

To může být ovšem v kontrastu k immutable typům matoucí - ukažme si to na obdobném příkladu

a = [1]

def f(x):
    x.append(2)

f(a)
print(a)
[1, 2]

Tedy neuvědomíme-li si, že funkci předáváme mutable proměnnou, může se nám její hodnota měnit “za zády”. Pozor je třeba dávat ještě na výchozí (defaultní) hodnoty argumentů funkcí. V příkladu níže vidíme, co se stane (každé slušné IDE na takový přešlap upozorní).

def f(x, lst = []):
    lst.append(x)
    return lst

lst1 = f(1, [1, 2, 3])
lst2 = f(2)
lst3 = f(3)
print(lst1, lst2, lst3)
print(lst2 is lst3)
[1, 2, 3, 1] [2, 3] [2, 3]
True

Na závěr se podívejme na tento humorný příklad. Co se tam děje?

a, b = (0, [1, 2]) , (0, [1, 2])
print(a == b, id(a) == id(b))
True False

Funkce jsou first class citizen#

Takto se označuje fakt, že funkce nemají oproti proměnným žádné výsadní postavení a můžeme tak s nimi nakládat. Tedy funkce je možné libovolně přiřazovat do nových proměnných.

Z hlediska OOP to znamená, že funkce je objekt, který má definovaný operátor __call__, jinými slovy, objekt, na které můžeme použít kulatou závorku.

def add(a, b):
    return a + b

f = add

f(1, 2)
3
def do_something_with_f(x, f):
    return f(x)

def f(x):
    return 2*x


do_something_with_f(2, f)
4

CPython data model a správa paměti#

Pod kapotou#

V této části si jen orientačně naznačíme, jakým způsobem jsou objekty v Pythonu vlastně implementované.

Každý objekt v Pythonu je ve skutečnosti napsán, jako struktura v jazyce C. Struktury nejsou nic jiného než složený datový typ, který sdružuje více proměnných pod jedním jménem. Konkrétně jsou všechny objekty něakou variací na strukturu PyObject. Z pohledu OOP jsou všechny objekty v Pythonu potomkem jednoho společného předka, a sice objektu Object. V Pythonu 2 bylo dokonce nutné při definici vlastní třídy tento vztah explicitně zdůrazňovat (viz kapitola o dědičnosti).

class MyClass(Object):
    ...

V těchto C strukturách jsou uloženy všechny možné informace: informace o typu, počet referencí (uvidíme u správy paměti), seznamy s funkcemi na těchto strukturách definovanými atd. CPython intenzivně využívá pointer type casting (tj. předstírá, že pointer na strukturu v paměti ve skutečnosti ukazuje na jiný typ struktury), díky čemuž je možné implementovat takovou funkcionalitu, jako je například polymorphismus, ale za cenu toho, že výsledný kód je poměrně zdlouhavý a obsahuje velikou spoustu všelijakých kontrol. Přikládám úryvek z dokumentace

Note The explicit cast to destructor above is needed because we defined Custom_dealloc to take a CustomObject * argument, but the tp_dealloc function pointer expects to receive a PyObject * argument. Otherwise, the compiler will emit a warning. This is object-oriented polymorphism, in C!

My se tímto směrem ještě jednou vydáme a ukážeme si explicitně, jak v CPythonu vypadá sčítání dvou čísel, ale prozatím nám toto stačí.

Správa paměti#

V jazycích, jako je C, je nutné ručně řídit správu paměti, obzvlášť v případech, kdy např. velikost používaných polí není známa během compile time, ale až během run time. To obvykle probíhá ve dvou krocích - alokace a dealokace. Následující příklad v C alokuje paměť pro pole celých čísel.

int some_function(int n) {
    int * array = malloc(sizeof(int) * n);

    if (array == NULL)
        return SOME_ERROR_CODE;

    process_array(array, n);
    
    free(array);
}

Manuální správa paměti nám sice dává velkou kontrolu, ale je velmi častým zdrojem chyb. Typickou chybou bývá opomenutí free, což znamená, že po skončení programu nebyla všechna paměť navrácena systému, tzv. memory leak error. Jinou častou chybou je opomenutí kontroly, zda alokace skutečně proběhla. Snažíme-li se pak např. zapisovat do této nealokované paměti, setkáme se s nechvalně známou segmentation fault, která už od low level programování odradila mnoho lidí.

V jazycích jako je Python, Java, JavaScript atd. se využívá naopak automatické správy paměti metodou, které říkáme garbage collection. Jako programátor se tedy od alokaci a dealokaci nestaráme a garbage collector to zajistí za nás - nevyužívané či nepřístupné objekty dekonstruuje a jim náležící paměť navrátí do poolu (Python si alokuje něco, čemu říká memory arenas, typicky po 256 KB, které dále pools po 4 KB. Tyto pools slouží pro alokaci objektů podobné velikosti, s cílem zamezit fragmentaci. V jednotlivých pools je paměť pro objekty alokována po blocích)

Python implementuje garbage collection pomocí reference counting - počítání referencí. Dokud je objekt nenulový počet referencí, zůstává v paměti, jakmile počet referencí klesne na 0, tj. na náš objekt nic neukazuje, je nedosažitelný, garbage collector ho smaže a pamět uvolní. Můžeme si to vyzkoušet pomocí modulu sys a zcela generického objektu.

import sys

x = object()
print(sys.getrefcount(x))
y = x
print(sys.getrefcount(x))
del x
print(sys.getrefcount(y))
2
3
2

Otázka: proč to např. pro x=1 dopadne takhle?

import sys

x = 1
sys.getrefcount(x)
3563

Generace#

Python aplikuje něco, čemu se občas říká Generation hypothesis, tedy předpoklad, že většina objektů zahyne krátce po svém vzniku. Pokud náhodou nepřežije, pak se přesune do další generace. Pythoní GC udržuje 3 generace objektů - Generation 0, 1, 2, někdy nazývané young, middleaged a old.

GC zahají sběr - postupně prochází všechny objekty a pokud mají 0 referencí, poznačí si je na seznam k likvidaci. Pokud mají více referencí a jsou tedy dosažitelné, přesune je do starší generace.

A kdy se vlastně sběr iniciuje? to je dáno počtem objektů v každé generaci. Každá generace má definovaný threshold, který když je překročen, proběhne sběr v dané generaci. To si můžeme prohlédnout prostřednictvím modul gc. Funkce get_threshold() nám vrátí aktuální threshold pro jednotlivé generace. Hodnoty můžeme měnit pomocí funkce set_threshold(young, mid, old), ale obecně platí, že na chod GC bychom spíše neměli sahat, pokud k tomu nemám opravdu dobrý důvod. Zatím jsem nikdy neměl ani špatný důvod na chod GC sahat.

import gc
# gc.set_threshold(1000, 15, 15)
gc.get_threshold()
(700, 10, 10)

Aktuální počet objektů v každé generaci si můžeme zobrazit a můžeme se rozhodnout i iniciovat kolekci manuálně.

import gc

print(gc.get_count())
gc.collect()
print(gc.get_count())
(311, 8, 6)
(18, 0, 0)

Self reference a cykly#

Problém nastane, když se v referencí objeví cyklus. Ukažme si jednoduchý příklad:

import sys

x = []
x.append(x)

print(sys.getrefcount(x))
del x
3

Po vymázání proměnné x stále zůstává jedna aktivní reference - první prvek listu referuje k listu. Pokud by GC kontroloval jen počet referencí, list x by v paměti zůstal a nebylo by ho jak odstranit. Pythoní algoritmus na detekci cyklů je popsán zde: https://devguide.python.org/internals/garbage-collector/ Zde to zatím rozebírat nebudu.

Řízení běhu programu#

Tato kapitola pokrývá zásadní stavební kameny každého programu: cykly a podmínky.

Podmínky#

Podmínky jsou jedním z hlavních nástroj používaných pro řízení běhu programu. Dovolují nám vykonat různé příkazy v závislosti na tom, zda je splněna nějaká podmínka. Tomuto větvení se obykle říká branching. Nejjednodušší podmínka má formu

if condition:
    ...

kde condition je jakýkoliv výraz či proměnná logického typu (nebo který/kterou je možné na logický typ konvertovat). Je-li výsledkem hodnota True, vykoná se následující blok příkazů. Například:

a = 4

if a < 10:
    print("proměnná 'a' je menší než 10")
proměnná 'a' je menší než 10

Podobně jako v ostatních jazycích můžeme vytváře komplikovanější rozhodovací strukturu s využitím konstrukcí elif a else. elif dovoluje přidat jednu nebo více následujících podmínek. Podmínka v elif se vyhodnocuje pouze v případě, že žádná z předchozích podmínek nebyla splněna. To samé platí i pro blok else, který však žádnou podmínku nepřidává. Celou strukturu je možné schématicky shrnout takto

if condition1:
    # block_1
elif condition_2:
    # block_2
.
.
.
elif condition_n:
    # block_n
else:
    # else block

Warning

Jestliže je splněno více podmínek v sérii if-elif-..., vykoná se pouze blok u první splněné podmínky:

a = 8

if a > 5:
    print("proměnná 'a' je větší než 5")
elif a < 10:
    print("proměnná 'a' je menší než 10")
proměnná 'a' je větší než 5

Ternární operátor#

Přestože spojení ternární operátor pouze označuje operátor, který působí na tři objekty (operandy) najednou (srovnejte s unárním a binárním operátorem), v kontextu programování se tím obvykle myslí zkrácený zápis konstrukce if-else. V Pythonu má podmínka zapsaná pomocí ternárního ooperátoru následující podobu:

a = 5

parity = "even" if (a % 2) == 0 else "odd"
print(parity)
odd

Logické operátory, operátory porovnání a logické výrazy#

Podmínky, na základě kterých se branching provádí, mají podobu logický výrazů (logical expressions), terdy výrazů, jechž výsledkem je hodnota logického typu, nebo kterou lze na logický typ převést.

Logické hodnoty jsou typicky výsledkem operací porovnávání, logických operací, typových konverzí nebo výstupem z nějaké funkce.

Mezi operátory porovnávání patří:

  • == rovnost hodnoty,

  • is rovnost identity,

  • <, <= menší, menší rovno,

  • >, := větší, větší rovno

Jejich význam je celkem očividný. Ve všech případech se jedná o binární operátory (tedy porovnávají dva objekty) a výsledkem je hodnota True, jestliže je jimi reprezentovaný vztah platný. Tedy například

a = 1
b = 2

a < b, a == b, a is b
(True, False, False)

Složitější logické výrazy lze skládat z jednodušších prostřednictvím logických operátorů, kterými jsou

  • and logické “a zároveň”, konjunkce,

  • or logické “nebo”, disjunkce,

  • not negace.

Konjunkce a disjunkce jsou opět binární, negace je unární (působí na jediný objekt).

a = 3

if (a > 0) and (a < 5):
    print("proměnná 'a' leží v otevřeném intervalu (0, 5)")
proměnná 'a' leží v otevřeném intervalu (0, 5)

Note

Závorky v předchozí ukázce nejsou povinné, ale často je pomocí nich možné zvýšit čitelnost výrazu.

Často je užitečné z hlediska čitelnosti či možnosti opakovaného využití “schovat” podmínku do nějaké funkce. Takové funkce typicky pojmenováváme se prefixem is_ a jejich návratovým typem je logická hodnota.

Následující příklad testuje, zda je číslo sudé, nejprve pomocí napřímo zapsané podmínky, potom za pomocí funkce is_even. Kód je sice mírně delší, ale je čitelnější a jeho záměr/smysl je patrnější.

a = 10

if (a % 2) == 0:
    print("číslo v proměnné 'a' je sudé")
číslo v proměnné 'a' je sudé
def is_even(num):
    return (num % 2) == 0

a = 10

if is_even(a):
    print("číslo v proměnné 'a' je sudé")

b = 11

if not is_even(b):
    print("číslo v proměnné 'b' je liché")
číslo v proměnné 'a' je sudé
číslo v proměnné 'b' je liché
Implicitní konverze a pravdivostní hodnota#

Pokud se na místě condition nenáchází výraz logického typu, Python se jej pokusí implicitně konvertovat. Z toho důvodu je dobré mít na paměti, jak se jednotlivé datové typy na bool konvertují (případně v závislosti na své hodnotě).

Typicky se jako False vyhodnocují prázdné kolekce, typ NoneType nebo veškeré “nulové hodnoty”.

if not ([] or () or "" or {} or None or 0 or 0.0 or 0+0j):
    print("vše se vyhodnotilo jako False")
vše se vyhodnotilo jako False
Redundance v logických výrazech#

Mechanismy uvedené výše je dobré mít stále na paměti. Pokud totiž budeme rozumět tomu, že v podmínkách Python zajímá logická hodnota vzniknuvší ať už konverzí, či prostým vyhodnocením výrazu condition, můžeme se vyvarovat následující sémantické redundance (významové nadbytečnosti)

some_bool = True

if some_bool == True:
    # do something

Explicitní porovnávání s logickou hodnotou žádnou novou informaci nepřináší, ba naopak je spíše matoucí. Následující je zcela ekvivalentní předchozímu.

some_bool

if some_bool:
    # do something

V závažnějších případech, ukažme si to například na funkce is_even, může taková redundance vypadat třeba takto:

def is_even(num):
    if ((num % 2) == 0) == True:
        return True
    else:
        return False

Už samotný výraz (num % 2) == 0 má tu logickou hodnotu, která nás zajímá. Vše ostatní je zbytečné. Tady, jako jsme viděli výše:

def is_even(num):
    return (num % 2) == 0

Podmínky na kolekcích#

Někdy je užitečné ověřit splnění podmínky na všech prvcích nějaké kolekce. Uvažujme jednoduchý příklad: mám seznam čísel a chceme se ujistit, že jsou všechna kladná. To lze udělat dvojím způsobem:

  1. je-li každé číslo kladné, pak jsou skutečně všechna kladná (to je v podstatě tautologie),

  2. je-li alespoň jedno číslo záporné, pak víme, že nejsou všechna kladná.

Druhou variantu by bylo lze “naivně” zapsat takto:

numbers = [1, 2, 3, 4, -5]

def are_all_positive(numbers):
    for x in numbers:
        if x < 0:
            return False
    return True

are_all_positive(numbers)
False

Python nabízí dvě pomocné funkce, jejichž význam je patrný z jejich názvu: all a any. Pomocí nich můžeme předchozí funkci implementovat hned dvěma způsoby.

numbers = [1, 2, 3, 4, -5]

def are_all_positive_1(numbers):
    return all(x > 0 for x in numbers)

def are_all_positive_2(numbers):
    return not any(x < 0 for x in numbers)

are_all_positive_1(numbers), are_all_positive_2(numbers)
(False, False)

Zde navíc používáme něco, čemu se říká generator expression. S tím se detailněji seznámíme v pozdějších kapitolách.

Iterace a cykly#

Slovo iterace znamená opakování. V kontexu programování tím většinou myslíme jednu ze dvou věcí:

  1. iterativní výpočet

  2. průchod přes prvky nějaké kolekce (častější)

Iterativním výpočtům se zde věnovat nebudeme, zaměříme se pouze na prochází kolekcí. Ukážeme si různé způsoby - některé budou více v duchu jazyka Python (tomu se na internetu říká pythonic), některé méně. Budeme se snažit používat spíše ty více pythonic varianty.

Tuple packing and unpacking#

Jako tuple packing se nazývá následující chování

t = 1, True

print(t, type(t))
(1, True) <class 'tuple'>

Přiřazením více hodnot do jedné proměnné vzniká proměnná typu tuple, která obsahuje všechny honoty. S tím se typicky setkáváme u funkcí, které navrací více hodnot.

def f():
    return 1, 2

t = f()
print(t, type(t))
(1, 2) <class 'tuple'>

Opačný proces, nazývaný tuple unpacking (případně obecněji sequence unpacking), slouží k opačnému účelu - zpřístupnění jednotlivých prvků sekvence v oddělených proměnných.

t = (1, 2)

a, b = t

print(a, b)
1 2

Pokud máme zájem pouze o některou složku, můžeme využít dummy proměnnou, pro kterou se v Pythonu používá podtřžítko.

t = (1, 2)

a, _ = t
print(a)
1

Vtipné je, že pod podtržítkem se ukrývá normální proměnná

t = (1, 2)

a, _ = t
print(_, type(_))
2 <class 'int'>

Můžeme podtržítko opakovat i kombinovat s *

a = tuple(range(10))

x, _, _, y, *_, z = a
print(x, y, z)
0 3 9

Cykly#

Základní cykly v Pythonu vypadají jako v ostatních jazycích

for i in range(5):
    print(i)
0
1
2
3
4

range(start, stop, step) je příkladem takzvaného generátoru (vysvětlíme později), který generuje postupně hodnoty od start po stop (bez) s krokem step. Příklad výše můžeme přepsat i jako while cyklus, který lze obecně zapsat jako

while condition:
    ...

Tedy tělo cyklu se vykonává, dokud je splněna podmínka condition.

i = 0
while i<5:
    print(i)
    i += 1
0
1
2
3
4
Break a continue#

K další kontrole nad chodem cyklu můžeme využít klíčová slova continue nebo break. continue přeskočí zbytek těla a přejde k další iteraci, break ukončí chod cyklu zcela. Srovnej následující příklady

for i in range(5):
    if i%2 == 0:
        continue
    print(i)
1
3
for i in range(5):
    if i == 3:
        break
    print(i)
        
0
1
2

Python nabízí i celkem neobvyklé rozšíření obou druhů cyklu - blok else, který si vykoná, jakmile na konci běhu. Pokud ale cyklus předčasně ukončíme pomocí break, blok else se nevykoná. Ukažme si to na while cyklu.

i = 0
while i<5:
    print(i)
    i+=1
else:
    print("cycle has finished")
0
1
2
3
4
cycle has finished
i = 0
while i<5:
    if i == 3:
        break
    print(i)
    i+=1
else:
    print("cycle has finished")
0
1
2

Iterace přes kolekce#

Procházení přes prvky kolekce lze realizovat i c-style, tedy postupným indexováním

colors = ["blue", "red", "green"]

for i in range(len(colors)):
    print(i, colors[i])
0 blue
1 red
2 green

Ale obecně se to považuje za nepříliš pythonic. V Pythonu lze přes všechny kolekce iterovat přímo

colors = ["blue", "red", "green"]

for c in colors:
    print(c)
blue
red
green

Všimněte se, že obyčejný for cyklus s range je vlastně úplně to samé - range lze v tomto smyslu považovat za kolekci. Pokud bychom v těle cyklu potřebovali aktuální index, používá se wrapper enumerate, který při iterací vrací tuple složený z indexu a hodnoty. Běžně se využívá v kombinaci s tuple unpacking.

# pythonic
colors = ["blue", "red", "green"]

for i, color in enumerate(colors, start=1):
    print(i, color)
1 blue
2 red
3 green

Podobně se používá i wrapper zip, kterým můžeme procházet více kolekcí najednou bez nutnosti indexovat.

a = [1, 2, 3, 4]
b = ["a", "b", "c"]

for num, letter in zip(a, b):
    print(num, letter)
1 a
2 b
3 c

Hint

Není to závazné, ale v Pythonu se budeme snažit v cycklech nezavádět indexy, pokud je nebudeme explicitně potřebovat. A pokud je zavádět budeme, použijeme k tomu konstrukce jako enumerate. Je to ustáleným zvykem, tak bychom ho měli respektovat.

Složitější iterace - itertools#

itertools je jedním z modulů Python Standard Library a umožňuje nám konstruovat složetější druhy iterátorů. Možností je mnoho, ukážu jen pár příkladů.

from itertools import product

a = [1, 2]
b = ["a", "b", "c"]

for num in a:
    for let in b:
        print(num, let)

for num, let in product(a, b):
    print(num, let)
1 a
1 b
1 c
2 a
2 b
2 c
1 a
1 b
1 c
2 a
2 b
2 c
from itertools import permutations

a = [1, 2, 3, 4]

for x in permutations(a):
    print(x)
(1, 2, 3, 4)
(1, 2, 4, 3)
(1, 3, 2, 4)
(1, 3, 4, 2)
(1, 4, 2, 3)
(1, 4, 3, 2)
(2, 1, 3, 4)
(2, 1, 4, 3)
(2, 3, 1, 4)
(2, 3, 4, 1)
(2, 4, 1, 3)
(2, 4, 3, 1)
(3, 1, 2, 4)
(3, 1, 4, 2)
(3, 2, 1, 4)
(3, 2, 4, 1)
(3, 4, 1, 2)
(3, 4, 2, 1)
(4, 1, 2, 3)
(4, 1, 3, 2)
(4, 2, 1, 3)
(4, 2, 3, 1)
(4, 3, 1, 2)
(4, 3, 2, 1)
from itertools import accumulate
a = [1, 2, 3, 4]

suma = 0
for x in a:
    suma += x
    print(suma)


for x in accumulate(a):
    print(x)
1
3
6
10
1
3
6
10
from itertools import groupby

log_entries = [
    {'timestamp': '2023-09-24 12:01', 'message': 'Started application', 'severity': 'INFO'},
    {'timestamp': '2023-09-24 12:02', 'message': 'User login', 'severity': 'INFO'},
    {'timestamp': '2023-09-24 12:03', 'message': 'File not found', 'severity': 'ERROR'},
    {'timestamp': '2023-09-24 12:04', 'message': 'Memory usage high', 'severity': 'WARNING'},
    {'timestamp': '2023-09-24 12:05', 'message': 'User logout', 'severity': 'INFO'},
    {'timestamp': '2023-09-24 12:06', 'message': 'Disk space low', 'severity': 'WARNING'}
]

for severity, group in groupby(log_entries, key=lambda x: x['severity']):
    print(f"Severity: {severity}")
    for entry in group:
        print(f"  {entry['timestamp']} - {entry['message']}")
Severity: INFO
  2023-09-24 12:01 - Started application
  2023-09-24 12:02 - User login
Severity: ERROR
  2023-09-24 12:03 - File not found
Severity: WARNING
  2023-09-24 12:04 - Memory usage high
Severity: INFO
  2023-09-24 12:05 - User logout
Severity: WARNING
  2023-09-24 12:06 - Disk space low

List and dict comprehension, generator notation#

Jedná se o tzv. syntactic sugar, motivovaný matematickou notací zápisu množin, která se nazýva set comprehension. Jak víme, v matematice lze množin zadat buď výčtem prvků, nebo charakteristickou vlastností. Např. množinu \(A = \{ 1, 2, 3, 4 \}\) můžeme zapsat jako \(A = \{ x \in N ; x < 5 \}\), což je právě ta charakteristická vlastnost. Ukažme si to na jednoduchém příkladu v Pythonu.

lst = []
for x in range(5):
    lst.append(2*x)

print(lst)

lst = [2*x for x in range(5)]

print(lst)
[0, 2, 4, 6, 8]
[0, 2, 4, 6, 8]

V list comprehension lze použít i podmínky. Obecně můžeme přepsat bloky následující struktury podle fixního mustru

src = # nejaka kolekce

res = []
for x in src:
    if condition:
        res.append(expression1(x)) # do listu pridame nejaky vyraz s x
    else:
        res.append(expression2(x)) # do listu pridame jiny vyraz s x

# ekvivalentni postup zapsany pomoci list comprehension vypada takto:
res = [expression1(x) if condition else expression2(x) for x in src]

Mírně odlišný zápis se používá pro jednodušší podmínky:

res = []
for x in src:
    if condition:
        res.append(expression(x)) # do listu pridame nejaky vyraz s x

# ekvivalentni postup zapsany pomoci list comprehension vypada takto:
res = [expression(x) for x in src if condition]
src = [3, -5, 0, -3, 2]
res = []
for x in src:
    if x > 0:
        res.append(x**2)
    else:
        res.append(0)

print(res)
[9, 0, 0, 0, 4]
# list comprehension
src = [3, -5, 0, -3, 2]

res = [x**2 if x >= 0 else 0 for x in src]

print(res)
[9, 0, 0, 0, 4]

Naprosto analogický zápis funguje pro tvrobu slovníků - takzvaný dictionary comprehension. Jediný rozdíl je v tom, že slovník je tvořen dvojicemi key: value, které tedy musíme definovat, a používá složené závorky

keys = ["a", "b", "c"]
vals = [1, 2, 3]

d = {}
for k, v in zip(keys, vals):
    d[k] = v
    
print(d)
{'a': 1, 'b': 2, 'c': 3}
keys = ["a", "b", "c"]
vals = [1, 2, 3]

d = {k: v for k, v in zip(keys, vals)}

print(d)
{'a': 1, 'b': 2, 'c': 3}
opts = {
    "opt_a" : 1,
    "opt_b" : 2,
    "opt_c" : 3
}

opts2 = {key[-1]:val for key, val in opts.items()}
print(opts2)
{'a': 1, 'b': 2, 'c': 3}
Make it more pythonic#

Konstrukce list/dict comprehension si obecně považuje za poměrně pythonic. V některých případech mohou být rychlejší než použití obyčejných cyklů, ale ten pythonic aspect bývá posuzován spíše z hlediska nějaké coding culture a čitelnosti.

Osobní názor

Pokud rychlost Vašeho programu zásadně ovlivňuje použití list/dict comprehension vs. klasický cyklus, děláte buď něco špatně, nebo je načase sáhnout po jiném jazyku.

Ukažme si na jednoduché úloze, jak lze “hloupě” napsaný kód trochu začistit a učinit více pythonic. Napišme funkci count_vowels(text), která spočítá, kolik je ve vstupním textu samohlásek, přičemž pro jednoduchost se omezíme na předpoklad, že v textu jsou pouze malá písmena.

První nástřel implementace by mohl vypadat takto:

def count_vowels(text):
    count = 0
    for c in text:
        for v in "aeiyou":
            if c == v:
                count += 1
    return count

count_vowels("hello")
2

Ty vnořené cykly jsou ale trochu ošklivé. Vzpomeneme si na operátor in, který nám umí říct, zda se objekt nachází uvnitř kolekce (string je totiž taky kolekce):

def count_vowels(text):
    count = 0
    for c in text:
        if c in "aeiyou":
            count += 1
    return count

count_vowels("hello")
2

Note

Ono k tomu cyklu stejně dojde, ale zde je implicitní - Python ho provede za nás.

Pomocí list comprehension se nám podaří implementaci ještě trochu zkrátit

def count_vowels(text):
    return len([c for c in text if c in "aeiyou"])

count_vowels("hello")
2
A není to moc pythonic?#

Někteří lidé mají tendenci začít vše zapisovat pomocí list comprehensions. Od určité chvíle to ale začíná působit opačně - kód přestává být čitelný a srozumitelný. Pojďme vzít funkci is_prime, která rozpozná, zda je číslo prvočíslem

def is_prime(n):
    if n <= 1:
        return False
    
    for i in range(2, n):
        if (n % i) == 0:
            return False
    return True

a použijme ji ke konstrukci funkce get_primes(numbers), která ze seznamu numbers vybere pouze prvočísla. Následují čtyři implementace, které dělají totéž, ale a přistupují k věci různě. Implementace 4 se snaží být pythonic za každou cenu, ale už se to nedá číst.

def get_primes_1(numbers): # neco jako: filter(is_prime, numbers)
    primes = []
    for num in numbers:
        if is_prime(num):
            primes.append(num)
    return primes

def get_primes_2(numbers):
    return list(filter(is_prime, numbers)) # very pythonic, funkcionalni pristup (functional programming)

def get_primes_3(numbers):
    return [x for x in numbers if is_prime(x)] # list comprehension, very very pythonic

def get_primes_4(numbers):
    return [x for x in numbers if all([(x%i != 0) for i in range(2, x)]) and x > 1] # this is too pythonic

Osobní názor

Já považuji za nejčitelnější implementace 2 a 3. Sám nejsem velký fanda funkcionálního programování, takže dávám přednost implementace 3, ae ale význam implementace 2 je stále velmi srozumitelný.

Nutno přiznat, že čitelnost implementace 4 by byla lepší, kdybychom použili lepší formátování (ale ne o moc).

def get_primes_4(numbers):
    return [
        x for x in numbers
        if all([
            (x%i != 0) for i in range(2, x)
            ]) and x > 1
        ]

Generátory#

O generátorech budeme více mluvt později, ale na tomto místě se je sluší zmínit. Nahradíme-li v zápisu list comprehension hranaté závorky kulatými, nebude výsledkem list, ale takzvaný generátor. Generátory jsou objekty, které umí říct, co je aktuální prvek a jak spočítat následující. To je výhodné v případně, kdy bychom chtěli vytvořit rozsáhlou kolekci. Generátor generuje prvky postupně, zatímco list je musí všechny předpřipravit, což může trvat dlouho a bude to zabírat hodně paměti.

Srovnejte čas, který zabere vykonání následujícího příkladu.

MAX = 100_000_000
gen = (2 * x for x in range(MAX))
print("done")

lst = [2 * x for x in range(MAX)]
print("done")
done
done

Při kompilace této knihy se první done objevilo takřka okamžitě, druhé se zpožděním cca 9 vteřin. Vyzkoušejte si na svém stroji.

Match#

Velkou novinkou od verze 3.10 je konstrukce match. Na první pohled bychom ji mohli vnímat jako obyčejnou analogii klasického switch, např.

usr_input = 2

match usr_input:
    case 1:
        print("doing thing number 1")
    case 2:
        print("doing thing number 2")
    case 3:
        print("doing thing number 3")
    case _:
        print("unknown input, doing the default thing")
doing thing number 2

Ve skutečnosti je match ale mnohem mocnější, není to pouhý přepínač (switch), ale konstrukce, která umí rozpoznávat složitější vzorce (structural pattern matching). Ukažme si to na příkladu jednoduchého command line interface:

def run_command(command: str) -> None:
    match command.split():
        case ["start"]:
            print("starting my thing")
        case ["quit" | "exit"]:
            print("quitting the program")
        case ["save", filename]:
            print(f"saving to {filename}")
        case ["export", ("pdf" | "jpg" | "txt") as output]:
            print(f"exporting stuff to {output}")
        case ["c" | "complicated", *args]:
            print(f"running the complicated command with many arguments: {args}")
        case _:
            print(f"unrecognized command {command}")
            
run_command("start")
run_command("c 1 2 jogurt")
run_command("save results.json")
run_command("export pdf")
run_command("export png")
run_command("quit")
starting my thing
running the complicated command with many arguments: ['1', '2', 'jogurt']
saving to results.json
exporting stuff to pdf
unrecognized command export png
quitting the program

Toto se dá zobecnit i na složitější object matching. (Co je dataclass, class a @ se dozvíme později, zatím to prostě akceptujte)

from dataclasses import dataclass

@dataclass
class Point:
    x: int
    y: int

p1 = Point(1, 0)
p2 = Point(1, 1)

def classify_point(p: Point):
    match p:
        case Point(x, 0):
            print("that is a point on the x axis")
        case Point(0, y):
            print("that is a point on the y axis")
        case Point(x, y) if x==y:
            print("that is a point on the diagonal")

classify_point(p2)
that is a point on the diagonal

Případně na něco takového

candidates = [{
    "name": "Vaclav",
    "age": 21,
    "language": ["czech", "english", "german"]
},
{
    "name": "Emmanuel",
    "age": 27,
    "language": ["french", "english", "german"]
},
{
    "name": "Grzegorz",
    "age": 35,
    "language": ["polish"]
}
]

def accept_candidate(p: dict):
    match p:
        case {"language": languages} if "french" in languages:
            return False
        case {"age": int(age)} if age < 25:
            return False
    return True
            
print(list(filter(accept_candidate, candidates)))
[{'name': 'Grzegorz', 'age': 35, 'language': ['polish']}]

Skvělý tutoriál najdete v PEP 636 a hezké video o Python match zde.

Funkce#

Tato kapitola pojednává o funkcích.

Funkce#

Funkce jsou bloky kódu určené k opakovanému použití. Každá funkce má název, může přebírat žádný, jeden nebo více parametrů a může, ale nemusí, něco navracet. Funkci zavedeme definicí, která je uvozena klíčovým slovem def. Parametry se definují pomocí názvů v kulaté závorce za názvem funkce. Případná návratovou hodnotu vracíme pomocí klíčového slova return.

def add(x, y):
    return x + y

add(1, 2.0)
add("A", "B")
'AB'

Note

Někdy se poněkud striktněji rozlišuje slovo parametr a argument. Parametr je v definici funkce a slouží jako placeholder (zástupný symbol) pro hodnoty, které pak funkcím předáváme. Jako argumenty se označují konkrétní hodnoty, které funkci předáváme.

Osobní názor

Většinu času podle mě není důležité tyto pojmy rozlišovat a ani ve většině této knihy tak nečiním. Ostatně, jak uvidíme ještě v této kapitole, přiliš je nerozlišuje ani dokumentace.

Protože je Python dynamicky typovaný, nezáleží obecně na tom, jaké datové typy do jednotlivých argumentů předáváme, a dokud jsou všechny operace s danými parametry definované i pro konkrétní argument (do you see what I did here?), všechno proběhne bez problému.

V příkladu výše se parametry x a y prostě sčítají, což je operace definovaná pro mnoho datových typů. Proto příklad funguje pro čísla nebo stringy, ale už nebude fungovat například pro slovník.

Staticky typované jazyky něco takového obvykle nepodporují, nebo jen za cenu dodatečné práce. Napřiklad v jazyce C je nutné mít různé funkce pro různé datové typy:

int add_int(int x, int y) {
    return x + y;
}

float add_float(float x, float y) {
    return x + y;
}

Oproti tomu třeba C++ podporuje takzvané přetěžování funkcí (function overloading), díky kterému je možné pro různé implementace použít stejný název a kompilátor pak vybere tu správnou variantu:

int add(int x, int y) {
    return x + y;
}

float add(float x, float y) {
    return x + y;
}

add(1.0, 2.0); // kompilátor automaticky zavolá druhou variantu

Argumenty funkcí#

Výchozí argument#

Python dovoluje v definici funkce každému parametru přiřadit výchozí hodnotou. Pokud mu při volání funkce nepředáme jinou hodnotu, použije se tato výchozí.

def f(a, b=4):
    print(a, b)

f(1)
f(1, 2)
1 4
1 2
Positional arguments#

Jako poziční (positional) argumenty se obvykle označují ty, u nichž záleží na tom, na jaké pozici je předáváme (tj. v jakém pořadí). To jsou vlastně všechny argumenty, které jsme dosud viděli. V předchozím příkladě se tedy první argument vždy předá do parametru a, druhý do parametru b

Variable lengths arguments#

Spojení variable length arguments je poměrně standarním označením pro argumenty, jejichž počet není dopředu znám. Kromě Python něco takové podporuje např. C nebo dokonce BASIC.

V Python se variable length argumenty označují * a v těle funkce jsou dostupné jako tuple (například tady by asi bylo správné použít slovo “parametry”, ale snad nikdy jsem nepotkal spojení “variable length parameters”).

def fun(*args):
    print(args, type(args))
    
fun()
fun(1)
fun(1, True)
() <class 'tuple'>
(1,) <class 'tuple'>
(1, True) <class 'tuple'>

Ve smyslu popsaném výše jsou i variable length argumenty zároveň i pozičními argumenty, neboť záleží na jejich pořadí (ve výsledném tuple budou v jiném pořadí). Někteří autoři ani spojení variable length arguments nepoužívají a říkají prostě positional arguments.

V následujícím příkladu budeme předstírat, že v Pythonu není funkce sum. Všimněte si dalšího použití * při volání funkce suma. Zde slouží jako efektivní tuple unpacking pro potřebu předání argumentů. Samostatně to však použít nelze.

def suma(*numbers):
    soucet = 0
    for num in numbers:
        soucet += num
    return soucet

nums = [1, 2, 3, 4]
suma(*nums) # to same jako suma(nums[0], nums[1], ..., nums[len(nums)-1])
10
Keyword arguments#

Další variantou jsou keyword arguments (někdy named arguments*). Tím se označují argumenty, které předáváme společně s jejich jménem (tedy fakticky dvojice key-value). V definici funkce je značíme ** a v těle jsou přirozeně dostupné jako dict.

def fun(**kwargs):
    print(kwargs, type(kwargs))
    
fun(a=1, b=2)
{'a': 1, 'b': 2} <class 'dict'>

Podobně jako u variable length argumentů můžeme funkci předat rozbalený (unpacked) slovník, což značí ** ve volání funkce. Opět to v jiném kontextu nelze použít.

d = {
    "a" : 1,
    "c" : True
}

fun(**d)
{'a': 1, 'c': True} <class 'dict'>
Vše dohromady#

Ve funkci můžeme samozřejmě využívat všechny druhy parametrů najednou. Zde funkce log_message a několik použití s různými argumenty. Kombinace různých typů parametrů společně s výchozími argumenty nám umožňuje vytvářet funkce s poměrně bohatou signaturou.

def log_message(message, *tags, level="INFO", **metadata):
    print(f"[{level}] {message}")

    if tags:
        print("Tags:")
        for tag in tags:
            print(f"  - {tag}")

    if metadata:
        print("Metadata:")
        for key, value in metadata.items():
            print(f"  {key}: {value}")
            
log_message("App started")
log_message("User logged in", "user", "auth")
log_message("File not found", level="ERROR")
log_message("Data processed", "data", "processing", level="DEBUG", elapsed_time="2.3s", records=42)
[INFO] App started
[INFO] User logged in
Tags:
  - user
  - auth
[ERROR] File not found
[DEBUG] Data processed
Tags:
  - data
  - processing
Metadata:
  elapsed_time: 2.3s
  records: 42
Positional only, keyword only arguments#

Python dovoluje zadávat i poziční argumenty jako keyword argumenty. Ale jakmile jednou ke keyword argumentů přejdeme, nelze se již navrátit k pozičnímu zadávání. Poziční argumenty nemohou následovat po keyword argumentech.

def fun(a, b):
    return a + b

fun(1, b=2) # v pořádku
fun(a=1, b=2) # v pořádku
# fun(a=1, 2) # v nepořádku
3

Kromě toho je možné označit argumenty, které jsou buď positional-only (nelze je tedy zadat jako keyword), nebo keyword-only (nelze je zadat jako poziční).

Motivace za keyword-only je celkem jasná. U funkcí, které přebírají větší množství argumentů (např. ještě stejného typu), zlepšuje čitelnost, když explicitně uvedeme, kterému parametru kterou hodnotu přiřazujeme.

Motivace za positional-only je trochu složitější a jsou za ní historické důvody a potřeba kompatibility s knihovnami napsanými v C. Podrobně o tom pojednává PEP 570.

def f(a, b, /): # positional only
    print(a, b)

def g(*, a, b): # keyword only
    print(a, b)
    
f(1, 2) # v pořádku
# f(b=1, a=2) # v nepořádku
# g(1, 2) # v nepořádku
g(b=1, a=2) # v pořádku
1 2
2 1

First-class citizen#

Připomínka v souvislosti s Python object modelem. Všechno v Pythonu je objekt. Tedy i funkce. Funkci tak můžeme přiřazovat do jiných proměnných, posuzovat její identitu atd. Když jazyk se funkcemi nakládá stejně jako s ostatními prvky, říkáme, že funkce jsou first-class citizen.

def f(x):
    return 2 * x

print(f, type(f), id(f))

funkce = f

print(funkce, type(funkce), id(funkce))
print(f is funkce)
f(2), funkce(2)
<function f at 0x7f2875e2feb0> <class 'function'> 139811753230000
<function f at 0x7f2875e2feb0> <class 'function'> 139811753230000
True
(4, 4)

Scope#

Slovo “scope” se do češtiny překládá trochu obtížné, nejspíše by se dalo říct “obor platnosti”. Obvykle se i v češtině používá anglická varianta.

Pojem scope označuje oblast v programu, ve které je daný název nebo proměnná dostupná, známá.

Proměnné, funkce a objekty definované na nejširším možném scope (outermost, top-level, tedy mimo nevnořené do jiných definic) se nazývají globální a jsou přístupné v celém modulu i všude, kam modul importujeme. Proměnné, funkce a objekty definované v těle funkce, metody nebo třídy se nazývají lokální. Ty jsou dostupné pouze v bloku, ve kterém jsou definované. O příslušných oblastech pak mluvíme jako o lokálním či globálním scope.

a = 1 # globální proměnná

def f(x):
    return x + a # ke globální proměnné můžeme referovat i v lokálním scope

f(1)
2
def f(x):
    b = 1 # lokální proměnná
    return x + b # ke globální proměnné můžeme referovat i v lokálním scope

# b # skončí chybou - v globálním scope není dostupná

Lokální proměnná může mít stejný název jako globální, ale je to bad practice, protože to může být poněkud matoucí.

s = "global"

def f():
    s = "local"
    print(s)
    
print(s)
f()
print(s)
global
local
global

Chceme-li z lokálního scope ovlivnit globální proměnnou, je nutné ji označit klíčovým slovem global

s = "global"

def f():
    global s
    s = "local"
    print(s)
    
print(s)
f()
print(s)
global
local
local

Existuje ještě klíčové slovo nonlocal, které má podobný vliv jako global, ale odkazuje se pouze “o scope výš”. To se používá např. u vnořených funkcí

def g():
    s = "g-level"

    def f():
        s = "f-level"
        print(s)

    print(s)
    f()
    print(s)
    
g()
g-level
f-level
g-level
def g():
    s = "g-level"

    def f():
        nonlocal s
        s = "f-level"
        print(s)

    print(s)
    f()
    print(s)
    
g()
g-level
f-level
f-level

Closure#

Lambda funkce a higher order functions#

Lambda funkce je malá, anonymní funkce, definovaná pomocí klíčového slova lambda. Tělo lambda funkce může tvořit jediný výraz (single expression) a typicky se používají pro jednoduché, jednorázové operace. Obecná syntaxe lambda funkce je následující

lambda arguments: expression

Ukažme si to na příkladu sčítání:

add = lambda x, y: x + y

add(1, 2)
3

To je sice funkční ale nevhodný příklad. Lambda funkce je sice objekt, jako vše ostatní v pythonu, takže ji můžeme uložit do libovolné proměnné (jako zde do proměnné add), ale tím to již není anonymní funkce a postrádá jakoukoliv výhodu oproti běžné funkci. Např. Pycharm rovnou upozorní, že lambda funkci nemáte ukládat do proměnné a použít místo ní běžnou funkci.

Jak tedy lambda funkce používat? Vzpomeňme si například na filtrování kolecí. Chceme-li ze kolekce vybrat jen určité prvky, třeba sudá čísla, můžeme psát

lst = [1, 2, 3, 4, 5, 6, 7]

even = [x for x in lst if x % 2 == 0]
print(even)
[2, 4, 6]

Nebo můžeme využít zabudovanou funkci filter(function, iterable), která protřídí iterovatelný objekt pomocí funkce function. Funkce function(iterable_element) -> bool vrací True, má-li prvek zůstat. Tedy

lst = [1, 2, 3, 4, 5, 6, 7]

even = list(filter(lambda x: x % 2 == 0, lst))
print(even)
[2, 4, 6]

Note

I dokumentace uvádí, že filter(fuction, iterable) je ekvivalentní generátoru (x for x in iterable if function(x)), proto explicitně voláme ještě konverzi na list, abychom viděli, co v kolekci zůstalo.

Klasickým příkladem použití je řazení (sorting). Použijme k tomu zabudovanou funkce sorted(iterable, key). Výstupem je list seřazených prvků. Do nepovinného argumentu key můžeme předat funkci, která každému prvku kolekce přiřadí klíč, podle kterého se bude řadit.

lst = ['bb', 'c', 'aaa']

lst1 = sorted(lst)
print(lst1)

lst2 = sorted(lst, key=lambda x: len(x))
print(lst2)
['aaa', 'bb', 'c']
['c', 'bb', 'aaa']

Typing#

Typing#

Staticky vs. dynamicky typované#

Stručně: staticky typované jazyky provádí kontrolu typů předem (např. během kompilace), dynamicky typované jazyky během runtime.

Ukažme si, co nám dynamicky typované jazyky typicky dovolí, ale ve staticky typovaných to může být problematické.

def add(x, y):
    return x + y

add(1, 2.0)
add("A", "B")
'AB'

V jazyce C je nutné definovat zvlášť funkce pro různé datové typy argumentů.

int add_int(int x, int y) {
    return x + y;
}

float add_float(float x, float y) {
    return x + y;
}

add_?(10, 1.0)

Některé jazyka, jako např. C++, dovolují i tzv. function overloading (přetěžování funkcí), tj. definovat funkce se steným názvem, ale odlišnou signaturou. Obvykle se toho dosahuje formou name mangling, tedy komolení názvů, a správná varianta funkce se vybírá během kompilace.

int add(int x, int y) {
    return x + y;
}

float add(float x, float y) {
    return x + y;
}

add(10., 1.0)

Python je sice dynamicky typovaný jazyk, ale možnost statické analýzy kódu může usnadnit práci a pomoci předejít řadě chyb. Ke statické analýze můžeme použít nástroj mypy, vyvíjený mimo python samotný, ale s úzkými vazbami na standard. Počínaje verzí 3.5 se v Pythonu začínají objevovat syntatktické prvky inspirované právě mypy, které mají statickou analýzu usnadnit - nazýváme je type hints.

Type hints#

def add(x: int, y: int) -> int:
    return x + y

Tímto fakticky říkáme, že argumenty x a y očekáváme celočíselné a stejně tak návratovou hodnotu. Nutno zdůraznit, že ačkoliv je to zcela validní Python, tak tyto type hints (či type annotations) slouží pouze pro účely statické analýzy kódu, která není povinná (tedy pomocí nástrojů jako mypy) a během runtime nejsou vůbec vynucovány. Hle:

add("a", "b")
'ab'

Každé slušné IDE bude v nějaké míře statickou analýzu kódu využívat a bude upozorňovat, pokud bude někde nekonzistentní. Nutno dodat, že například mypy type hints ke svému chodu nutně nepotřebuje a dokáže lecos vyčíst z kontextu.

Type hints mohou zahrnovat i složitější konstrukce - v zásadě jakýkoliv typ (i složený) můžete pro potřeby type hints nějak reprezentovat. Hezký přehled najdete přímo na webu mipy ve formě cheatsheetu, zde uvedu jen pár příkladů.

def max(lst: list[float | int]) -> float | int:
    ...
Změny v posledních verzích#

Do verze Pythonu 3.9 bylo nutné pro popis složených typů importovat generic z podpůrného modulu typing. Tedy např. seznam celých čísel bylo nutné anotovat takto:

from typing import List

lst: List[int]

Od verze 3.9 už můžeme využívat standardní název typu

lst: list[int]

S verzí 3.10 mizí nutnost používat Union, když chceme povolit více různých typů. Např. seznam floatů či intů

lst: list[int | float]

zatímco dříve

from typing import Union

lst = list[Union[int, float]]
Duck typing#

If it walks like a duck, and it quacks like a duck, then it must be a duck.

Duck typing je přístup k typování podobný strukturnímu - objekt je považován za objekt správného typu, pokud má definovanou tu správnou sadu vlastností.

Tohoto principu je vhodné v pythonu využívat a volit vhodné type hints na základně toho, jaké vlastnosti od daného objektu vlastně chceme. Např. pokud má být možné přes objekt iterovat a jinou vlastnost nevyžadujeme, zvolíme typ Iterable

from typing import Iterable

def print_items(kolekce: Iterable) -> None:
    for i in kolekce:
        print(i)
        
print_items("ab")
a
b

Řadu příkladu nalezneme např. v mipy cheatsheet.

from typing import Callable

def get_primes(number: list[int], is_prime: Callable[[int], bool]) -> list[int]:
    ...

Type aliasing#

Složené typy lze i pojmenovávat a za některých okolností je to i vhodné. Například pokud v programu často pracujeme se souřadnicemi,můžeme si vytvořit vhodný typ. Anotace funkcí pak budou trochu čitelnější

Coords = tuple[float, float]

def distance(this: Coords, other: Coords) -> float:
    ...

V tomto případě se jedná pouze o alias a type checke nebude rozlišovat mezi Coords a tuple[float, float]. Pokud bychom chtěli nový, odlišný typ, je třeba použít NewType

from typing import NewType
Coords = NewType("2d coordinates", tuple[float, float])

Poznámka#

K typování se vrátíme ještě v kapitole o OOP - naučíme se psát tzv. protokoly, které slouží jako abstraktní prototypy pro duck typing.

Čtení ze souboru a užitečné balíčky#

Načítání ze souboru#

Základní operace#

file = open("../../examples/config.ini", "r")

data = file.read()

print(data)

file.close()
[section a]
logfile = path/to/logfile
url = www.google.com
do_print = true
params = [1, 2, 3]

soubor lze otevřít v několika modech:

  • ‘r’ pro čtení

  • ‘w’ pro zápis

  • ‘a’ pro zápis v režimu append - zápis na konec

ke každému lze přidat ještě modifier ‘b’ pro práci v binárním režimu

Soubor je po dokončení práce třeba vždy uzavřít. Pokud soubor ponecháme otevřený, může dojít k nečekaným výsledkům. V příkladu níže vidíme, že při načtení ze souboru pomocí handle f3 nevidíme změny zapsané pomocí handle f2.

from os import remove

filename = "tmpfile.txt"

f1 = open(filename,'w')
f1.write("content")
f1.close()

f2 = open(filename, 'a')
f2.write(' - incorrectly modified')

f3 = open(filename,'r')
print(f3.read())

f2.close()
f3.close()

remove(filename)
content

Zatímco pokud soubor vždy pečlivě uzavřeme, změny se zapíšou.

from os import remove

filename = "tmpfile.txt"

f1 = open(filename,'w')
f1.write("content")
f1.close()

f2 = open(filename, 'a')
f2.write(' - correctly modified')
f2.close()

f3 = open(filename,'r')
print(f3.read())
f3.close()

remove(filename)
content - correctly modified
  • dobrým zvykem je, zavírat otevřený soubor, jakmile ho dále nepotřebujeme

  • jakmile napíšete open, napište rovnou i close

with statement a context manager#

  • jednou možností, jak bezpečněji otevírat soubory, je with statement.

  • veškerá práce se odehrává v odsazeném bloku a po jeho skončení se soubor uzavře

  • https://peps.python.org/pep-0343/

with open("../../examples/config.ini", "r") as f:
    data = f.read()

for line in data.splitlines():
    print(line)
[section a]
logfile = path/to/logfile
url = www.google.com
do_print = true
params = [1, 2, 3]

with statement je ve skutečnosti operuje s něčím, co se nazývá context manager protocol. Context manager protocol naplňuje každý objekt, který má definované metody __enter__ a __exit__

Uzavírání zdrojů po dokončení práce se tradičně řeší pomocí bloku finally. A skutečně, řádky

with EXPR as var:
    BLOCK

jsou zhruba ekvivalentní řádkům

VAR = EXPR
VAR.__enter__()
try:
    BLOCK
finally:
    VAR.__exit__()

Předchozí příklad lze přepsat jako

f = open("../examples/config.ini", "r")
f.__enter__()
try:
    data = f.read()
finally:
    f.__exit__()
f = open("../../examples/config.ini", "r")
f.__enter__()
try:
    data = f.read()
finally:
    f.__exit__()
print(data)
[section a]
logfile = path/to/logfile
url = www.google.com
do_print = true
params = [1, 2, 3]

Přidejme si typický příklad - práce s databízí:

class DatabaseConnection:
    def __init__(self, db_name):
        self.db_name = db_name

    def __enter__(self):
        print(f"connecting to database {self.db_name}")
        return self
    
    def execute(self, query):
        print(f"executing query: {query}")
        
    def execute_with_exception(self, query):
        raise Exception(f"a generic exception for this example")

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is not None:
            print(f"Exception {exc_type} caught, rollin back changes")
        else:
            print("committing changes")
        print("closing connection")

V případě, že se nic nepokazí, je průběh přímočarý:

with DatabaseConnection("my_db.sqlite") as db:
    db.execute("CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, name TEXT);")
    db.execute("INSERT INTO my_table (name) VALUES ('Alice');")
connecting to database my_db.sqlite
executing query: CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, name TEXT);
executing query: INSERT INTO my_table (name) VALUES ('Alice');
committing changes
closing connection

Pokud ale dojde k výjimce (zde reprezentováno metodou execute_with_exception), připojení k databázi se normálně ukončí i přes to, že výjimka “probublá” ven.

try:
    with DatabaseConnection("my_db.sqlite") as db:
        db.execute("CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, name TEXT);")
        db.execute_with_exception("INSERT INTO my_table (name) VALUES ('Alice');")
except Exception:
    print("database query failed but database connection was safely closed")
connecting to database my_db.sqlite
executing query: CREATE TABLE IF NOT EXISTS my_table (id INTEGER PRIMARY KEY, name TEXT);
Exception <class 'Exception'> caught, rollin back changes
closing connection
database query failed but database connection was safely closed

Načítání známých datových typů#

Následuje orientační přehled formátů, které lze číst pomocí Python Standard Library - tj. není nutné nic instalovat. Seznam není vyčerpávající.

ini#

Formát ini je celkem známý, prostý formát konfiguračních souborů, příklad z Wiki:

; last modified 1 April 2001 by John Doe
[owner]
name = John Doe
organization = Acme Widgets Inc.

[database]
; use IP address in case network name resolution is not working
server = 192.0.2.62     
port = 143
file = "payroll.dat"

Problém s ini formátem je ten, že není příliš standardizování. Některé varianty nedovolují komentáře, jiné nedovolují sekce.

V Python je k dispozici balík configparser, který nějaké ini-like soubory číst umí.

Nevýhody:

  • nepříjemná práce

  • neidentifikuje datové typy hodnot

  • vytváří vlastní (sice iterovatelné) typy, ačkoliv by byl lepší slovník

import configparser

config = configparser.ConfigParser()
config.read("../../examples/config.ini")

for section in config.sections():
    for key, val in config[section].items():
        print(key, val, type(val))
logfile path/to/logfile <class 'str'>
url www.google.com <class 'str'>
do_print true <class 'str'>
params [1, 2, 3] <class 'str'>

Existuje alternativa - toml - Tom’s Obvious Minimal Language

  • podobný ini, ale s konkrétní specifikací

  • široká podpora napříč jazyky

  • rozeznává datové typy hodnot

  • vše dává k dispozici jako slovník

V Pythonu dostupný jako externí balík tomli, od verze 3.11 dokonce součástí Pythonu jako tomllib

import tomli

with open("../../examples/config.toml", "rb") as file:
    cfg = tomli.load(file)

print(cfg)
{'title': 'TOML Example', 'owner': {'name': 'Tom Preston-Werner', 'dob': datetime.datetime(1979, 5, 27, 7, 32, tzinfo=datetime.timezone(datetime.timedelta(days=-1, seconds=57600)))}, 'database': {'enabled': True, 'ports': [8000, 8001, 8002], 'data': [['delta', 'phi'], [3.14]], 'temp_targets': {'cpu': 79.5, 'case': 72.0}}, 'servers': {'alpha': {'ip': '10.0.0.1', 'role': 'frontend'}, 'beta': {'ip': '10.0.0.2', 'role': 'backend'}}}
json#

JSON - JavaScript Object Notation

specifikace json.org

  • v pythonu s pomocí knihovny json

  • formát JSON je mimořádně rozšířen

  • knihovna snadná na používání

  • má zabudovaný “pretty print”

import json

with open("../../examples/config.json", "r") as file:
    cfg = json.load(file)
    
for key, val in cfg["section a"].items():
    print(key, val, type(val))
    
print(json.dumps(cfg, indent=4))
logfile path/to/logfile <class 'str'>
url www.google.com <class 'str'>
do_print True <class 'bool'>
params [1, 2, 3] <class 'list'>
{
    "section a": {
        "logfile": "path/to/logfile",
        "url": "www.google.com",
        "do_print": true,
        "params": [
            1,
            2,
            3
        ]
    }
}
csv - comma separated values#

Další velmi rozšířený formát s nejasnou specifikací. Python nabízí ve standard library modul pro práci s csv: csv.

Osobní názor

S modulem csv se pracuje špatně. Je divný a omezený.

Trochu robustnější možnosti čtení csv souborů nabízí moduly jako numpy nebo pandas.

Užitečné balíčky#

Práce s cestami#

os.path#
  • low-level funkcionální přístup

import os

root = "/data/unicorn/uc-python"
full_path = os.path.join(root, "22_23_zs", "mipy")

print(full_path)
/data/unicorn/uc-python/22_23_zs/mipy
pathlib#
  • high-level objektový přístup

from pathlib import Path

root =  Path("/data/unicorn/uc-python")

full_path = root / "22_23_zs" / "mipy"
print(full_path)
for file in full_path.iterdir():
    if not file.is_dir(): print(file)
    
for file in full_path.glob("*_live*"):
    print(file)
/data/unicorn/uc-python/22_23_zs/mipy
/data/unicorn/uc-python/22_23_zs/mipy/05_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/config.ini
/data/unicorn/uc-python/22_23_zs/mipy/modul1.py
/data/unicorn/uc-python/22_23_zs/mipy/01.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/09_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/config.json
/data/unicorn/uc-python/22_23_zs/mipy/main.py
/data/unicorn/uc-python/22_23_zs/mipy/09.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/03_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/02_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/config.toml
/data/unicorn/uc-python/22_23_zs/mipy/07_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/02.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/10.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/app_signal.py
/data/unicorn/uc-python/22_23_zs/mipy/10_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/03.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/04_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/waves.py
/data/unicorn/uc-python/22_23_zs/mipy/07.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/pokus.json
/data/unicorn/uc-python/22_23_zs/mipy/receipt.py
/data/unicorn/uc-python/22_23_zs/mipy/good_receipt.py
/data/unicorn/uc-python/22_23_zs/mipy/app.py
/data/unicorn/uc-python/22_23_zs/mipy/05_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/09_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/03_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/02_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/07_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/10_live.ipynb
/data/unicorn/uc-python/22_23_zs/mipy/04_live.ipynb
shutil#

High-level file operations

  • kopírování

  • přesouvání

  • přístup k právům

  • atd

modul os#

print(os.getcwd())
os.chdir("/data/codebase")
print(os.getcwd())
/data/unicorn/python/book/content/files_and_packages
/data/codebase
os.getenv("PYTHONPATH")
os.getenv("PATH")
'/data/unicorn/pythonbook/venv/bin:/home/vaclav/.local/bin:/home/vaclav/.local/bin:/data/codebase/m2d/bin:/data/codebase/utils/bin:/data/codebase/mptc/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin:/snap/bin:/home/vaclav/.local/share/JetBrains/Toolbox/scripts:/home/vaclav/.local/share/JetBrains/Toolbox/scripts'

modul sys#

Modul sys je vestavěný modul, který poskytuje jistý interface mezi systémem a pythoním interpretem. Mezi nejčastější využití patří

Přebírání commad-line argumentů předaných skriptu.#
import sys

print(sys.argv)
['/home/vaclav/.local/lib/python3.10/site-packages/ipykernel_launcher.py', '-f', '/tmp/tmpy3sys83_.json', '--HistoryManager.hist_file=:memory:']

Toto je mimořádně matoucí tady v prostředí Jupyter. Pro lepší pochopení uložte předchozí kód do souboru a main.py a z příkazové řádky jej spusťte s několik argumenty navíc. Uvidíte toto:

$ python3 main.py arg1 arg2
['main.py', 'arg1', 'arg1']

Je to běžný způsob, jakým se programům předávají dodatečné argumenty. Pro složitější sadu argumentů se vyplací použít např. vestavěný modul argparse.

Ukončení programu s konkrétním exit code:#
import sys

sys.exit(0)
Přístup k informacím o interpretu či systému#
import sys

print(sys.version)
print(sys.version_info)
print(sys.platform)
3.10.12 (main, Nov 20 2023, 15:14:05) [GCC 11.4.0]
sys.version_info(major=3, minor=10, micro=12, releaselevel='final', serial=0)
linux

Modul datetime#

Vestavěný model poskytující běžné funkce pro práci s datem. Asi nejčastší operace, které budete s tímto balíkem provádět, jsou tyto:

  1. aktuální datum a čas

from datetime import datetime

now = datetime.now()
print(now)
2024-03-10 15:06:52.534840
  1. reprezentace konkrétního data

from datetime import datetime

specific_date = datetime(year=2023, month=10, day=22, hour=5)
print(specific_date)
2023-10-22 05:00:00
  1. formátování data

from datetime import datetime

specific_date = datetime(year=2023, month=10, day=22, hour=5, minute=30, second=3)
formatted_date = specific_date.strftime("%Y%m%d%H%M%S")
print(formatted_date)
20231022053003
  1. parsování data

date_string = "2023-10-22"
parsed_date = datetime.strptime(date_string, "%Y-%m-%d")
print(parsed_date)
2023-10-22 00:00:00
  1. práce s rozdíly (k tomu slouží samostatný objekt timedelta)

from datetime import datetime, timedelta

now = datetime.now()
future = now + timedelta(minutes=45)

print(now, future)
2024-03-10 15:06:52.563931 2024-03-10 15:51:52.563931

Logging#

Python poskytuje prostřednitvím modulu logging poměrně širokou podporu logování. V modulu existují různé úrovně logování, které určují závažnost logovacích zpráv: DEBUG, INFO, WARNING, ERROR a CRITICAL. Nižší úrovně (např. DEBUG) jsou určeny pro detailní informace, zatímco vyšší úrovně (např. CRITICAL) slouží pro označení vážných problémů, které vyžadují okamžitou pozornost. Obecně je lepší používat pořádný logging než sérii printů - v případě potřeby lze míru logování změnit volbou log level. Základní použití si ukažmě na příkladu

import logging

logging.basicConfig(
    format='%(asctime)s - %(levelname)s - %(message)s',
    level=logging.INFO,
    # filename='example.log'
)

logging.info("This is an info message")
logging.debug("This message will be ignored")
logging.warning("This is a warning")

logging.getLogger().setLevel(logging.DEBUG)
logging.debug("This message will not be ignored anymore")
2024-03-10 15:06:52,568 - INFO - This is an info message
2024-03-10 15:06:52,569 - WARNING - This is a warning
2024-03-10 15:06:52,569 - DEBUG - This message will not be ignored anymore

Modul logging standardně vypisuje všechno do stderr (proto to Jupyter označuje červeně). stdout (standardní výstup) a stderr (standardní chybový výstup) jsou dva datové toky, které jsou běžně používány pro interakci mezi programem a jeho vnějším prostředím. Zatímco stdout se obvykle používá pro běžný výstup programu, stderr se používá pro výpis chybových a diagnostických zpráv.

Velmi detailní a vtipný rozbor logování v Pythonu naleznete na kanále mCoding na YouTube ve videu nazvaném Modern Python logging. Následující pasáž a diagram níže je výtažkem nejdůležitějšího z onoho videa.

Logování je v pythonu rozděleno mezi několik rolí.

Architektura logging modulu, diagram od mCoding

  1. logger je hlavní objekt, se kterým interagujeme. Na něm voláme metody jako .info nebo .warning. Tím vznikají log records, které jsou předávány ostatním objektům k dalšímu zpracování.

  2. handler řídí, kam se vlastně log records zapisují. Tím “kam” mohou být soubory, standardní výstup stdout nebo např. email. Každý logger může mít libovolné množství handlers (i žádný).

  3. filter slouží k odfiltrování zpráv dle různých kritérií, např. s využitím regulárních výrazů. Filter lze definovat jak pro logger, tak pro handler, přičemž každý může mít více filterů.

  4. formatter určuje výslednou podobu zprávy - zda obsahuje čas, v jakém formátu, kde jsou jaké závorky atd.

Klíčovým prvkem logování je samozřejmě filtrování podle LEVEL. Jak handler, tak loggger zpracovávají pouze zprávy vyšší nebo stejné závažnosti, jako je jejich nastavený LEVEL.

Kromě toho Python ve svém logging modulu uspořádává loggery do stromové hierarchie, která může být principiálně velmi komplikovaná. My si z toho zatím vezmeme jedno ponaučení:

Tip

Nikdy nebudeme používat root logger, tedy logování prostřednictvím globáních funcí jako logging.info apod. Zejména pokud bychom měnili chování root loggeru, mohli bychom se dočkat nepříjemné interakce s logováním ostatních modulů.

Budeme vždy vytvářet vlastní loggery pomocí metody logging.getLogger(name).

Note

Logger je implementován jako singleton - to znamená, že pokud logger s názvem name neexistuje, funkce getLogger jej vytvoří. Pokud existuje, dostaneme referenci k existující instanci.

Detailní nastavení#

Logging lze podrobně konfigurovat pomocí slovníků. Dokumentace je v tomto ohledu mírně nepřehledná a tajemná. Ukažme si proto dva jednoduché příklady. Více příkladů s detailním výkladem najdete v zmiňovaném video z mCoding.

log_config = {
    "version": 1,
    "disable_existing_logger": False,
    "formatters": {
        "simple": {
            "format": "%(levelname)s: %(message)s"
        }
    },
    "handlers": {
        "stdout": {
            "class": "logging.StreamHandler",
            "level": "INFO",
            "formatter": "simple",
            "stream": "ext://sys.stdout"
        },
        "file": {
            "class": "logging.handlers.RotatingFileHandler",
            "level": "DEBUG",
            "formatter": "simple",
            "filename": "my_app.log",
            "maxBytes": 10000,
            "backupCount": 2
        }
    },
    "loggers": {
        "sample": {
            "level": "DEBUG",
            "handlers": [
                "stdout",
                "file"
            ]
        }
    }
}
import logging

logging.config.dictConfig(config=log_config)

logger = logging.getLogger("sample")
logger.warning("some warning")
logger.debug("some debug info")
WARNING: some warning
2024-03-10 15:06:52,580 - WARNING - some warning
2024-03-10 15:06:52,581 - DEBUG - some debug info

Pokud bychom chtěli logging pouze do stdout, musíme nastavit loggin handler. Těch je možné i více najednou. Následující příklad loguje do stdout a do souboru zároveň (a ukazuje alternativní cestu, jak nastavit logování).

import logging
import sys

logger = logging.getLogger('my_logger')
logger.setLevel(logging.DEBUG)

# create a StreamHandler for stdout
stdout_handler = logging.StreamHandler(sys.stdout)
logger.addHandler(stdout_handler)

# Log a message
logger.info("This message will go to stdout")
This message will go to stdout
2024-03-10 15:06:52,585 - INFO - This message will go to stdout

Warning

Kniha je napsaná a kompilovaná pomocí Jupyter Book. Protože logging používá singleton a Jupyter startuje jeden kernel per notebook, je pravděpodobné, že poslední příklad se v knize stále vypíše do stderr. Aby to prošlo správně, je nutné v mezičase kernel restartovat.

Výjimky#

Mechanismus výjimek#

Výjimky (exceptions)#

Python používá ke zpracování chyb výjimky. Ne každý jazyk to dělá stejně. Například v C se používají tzv. návratové kódy - funkce navrací různé číselné hodnoty v závislosti na tom, k čem během chodu došlo.

Je-li vyvolána výjimka, chod programu se zastaví a hledá se nejbližší catch blok (tedy sada příkazů zodpovědná za zpracování výjimek). Není-li v aktuálním scope takový blok přítomný, je ukončen a program se přesune do následujícího stack frame v call stack - neboli do scope funkce, ze které byla ta dosavadní volána. To se opakuje, dokud program nenarazí na odpovídají blok, který výjimku zpracuje, nebo dokud nevyčerpá celý call stack, v kterémž to případě program neúspěšně skončí. Tomuto procesu/mechanismu se říká stack unwinding.

Chyby v programu napsaném v Pythonu jsou v zásadě dvojího druhu:

  1. SyntaxError

  2. Všechny ostatní

K SyntaxError dochází ve fázi čtení, tj. když parser prochází zdojový kód. Je to tak jediná chyba, která se za normálních okolností nedá zachytit (někdy jo, ale o tom jindy). Všechny ostatní chyby lze zachytit a zareagovat na ně nějakým žádoucím způsobem. Začněme jednoduchým příkladem dobře čitelné chyby. Když dojde k neošetřené chybě, Pythoní interpret nám napíše, k jaké chybě došlo a vypíše její stack trace (někdy traceback), tedy kde k té chybě došlo

a = 1
b = 0
a / b # raises ZeroDivisionError
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
/tmp/ipykernel_1080382/1701595597.py in <module>
      1 a = 1
      2 b = 0
----> 3 a / b # raises ZeroDivisionError

ZeroDivisionError: division by zero

Zde vidíme, že chyba je typu ZeroDivisionError a došlo k ní na řádku 3. To je celkem čitelné. Zkusme chybu zabalit do nějaké funkce. a podívat se, jak se traceback změní.

def f(a, b):
    return a / b

f(1, 0)

Traceback je teď složitější, ale stále čitelný - postupuje zvenku dovnitř a ukazuje, v jaké funkci na jaké řádku k chybě došlo. To je velmi užitečné.

Try / except#

Chyby - výjimky - takzvaně zachytáváme a činíme tak pomocí dvojice bloků try a except. V bloku try spouštíme kód, u kterého máme podezření, že by mohl vyvolat (raise) nějakou výjimku, blokem except výjimku zachytáváme (v jiných jazycích se tento blok nazývá catch block).

def f(a, b):
    a / b

try:
    f(1, 0)
except ZeroDivisionError:
    print("oi, we do not divide by zero")

print("the rest of the program")

Vidíme, že přestože k chybě došlo, zachytili jsme ji a zpracovali, takže program nespadl. To je žádoucí stav, ke kterému se obvykle budeme snažit směřovat.

Jak zjistíme, jakou výjimku zachytit? Buď zkusíme kód spustit tak, aby selhal, a chybu najdeme na konci stack trace, nebo ji vyhledáme v dokumentaci - často potkáme fráze jako: “raises ThatAndThatError if beans not hot enough”.

Např. u typu dict se v dokumentaci píše:

Documentation excerpt

Vyzkoušejme:

d = {
    "a": 1,
    "b": 0
}

try:
    d["c"]
except KeyError:
    print("ejhle, ono to funguje")

print("zbytek programu...")

Můžeme také zřetězit několik except bloků za sebou a zachytávat postupně různé výjimky, nebo zachytit více výjimek v jednom except bloku.

from random import randint

def faulty_function():
    i = randint(1, 3)
    match i:
        case 1:
            raise KeyError("fake key error")
        case 2:
            raise ZeroDivisionError("fake zero division error")
        case 3:
            raise AttributeError("fake attribute error")

try:
    faulty_function()
except (KeyError, AttributeError):
    print("this is the first except block")
except ZeroDivisionError:
    print("this is the second except block")

Výjimka jako objekt#

Každou odchycenou výjimku můžeme lapit do proměnné a získat ještě dodatečné informace. Každá výjimka obsahuje mimo jiných atributy args (obsahující konkrétní parametry) a __traceback__ (obsahující stack trace). Abychom ale traceback mohli číst, potřebujeme použít modul traceback.

import traceback

def a_funky_function():
    d = {}
    d["non-existent-key"]

try:
   a_funky_function()
except KeyError as e:
    print(e.args)
    traceback.print_tb(e.__traceback__)

print("rest of the program")

Je možné zachytit i zcela obecnou výjimku - všechny výjimky jsou typu Exception:

try:
    1/0
except Exception as e: # toto zachytí jakoukoliv výjimku
    print(e.args)

Nikdy#

ale vážně nikdy (to znamená v žádném případě) a to tak, že za žádných okolností, neděláme toto:

try:
    1 / 0 # jakykoliv vadny kod
except:
    pass

Nezpracovaná, ale zachycěná výjimka znamená, že program nespadne, ale my o chybě vůbec nevíme. To znaměná, že se velmi špatně hledá.

Finally (a else)#

Kromě bloků try a except máme ještě bloky else a finally. Blok else následuje bezprostředně po except bloku (blocích) a vykoná se pouze v případě, že k žádné výjimce nedošlo. Blok finally je poslední a vykoná se vždy. Jeho smyslem je uklidit, tj. uzavřít otevřená spojení, navrátit zdroj atd. Následující příklad vše ilustruje - soubor se uzavře, ať už se stance v průběhu čtení cokoliv.

try:
    f = open("file.txt", "r")
    content = f.read()
except FileNotFoundError:
    print("File not found.")
except Exception as e:
    print("Some other error occured")
else:
    print("File read successfully.")
finally:
    if 'f' in locals(): # overi, jestli symbol f vubec existuje, tj. jestli se soubor vubec povedlo otevrit
        f.close()
    print("File closed.")

Vyvolání výjimky#

Výjimku samozřejmě můžeme i vyvolat. Používá se k tomu klíčové slovo raise. Seznam v pythonu dostupných výjimek nalezneme v dokumentaci - zde. S ohledem na kontext můžeme použít kteroukoliv z nich.

def my_function_raising_exceptions(x):
    if x == 0:
        raise ValueError("argument x is not supposed to be zero")

try:
    my_function_raising_exceptions(0)
except ValueError as e:
    print(e.args)

Kdy zachytit?#

Obecně se říká, že výjimka by se měla zachytit co nejblíže svému vzniku. Důvodů je řada, mezi hlavní patří např. kontext, za kterého výjimka vznikla (je dostupný, čitelný a výjimka snadno odstranitelná), čitelnost nebo třeba rychlost - v průběhu zpracování výjimky dochází k něčemu, čemu se říká stack unwinding - odvíjení zásobníku - Python postupně prochází zásobník volaných funkcí (call stack) a hledá, zda tam není někdo, koho ta výjimka zajímá (except blok). Když nikoho nenajde, program selže.

Následující série příkladů ilustruje, jaký vliv na celkový program má to, kde výjimku zachytíte. Můžete si k zachycení přípsat i traceback.

def may_throw():
    raise RuntimeError("oh no, it happened")
        
def subprogram():
    print("this is subprogram, doing subprogram things")
    may_throw()
    print("subprogram finished")
    
def mainprogram():
    print("this is the main program, doing main program things")
    print("calling subprogram")
    subprogram()
    print("mainprogram finished")
        
mainprogram()
def may_throw():
    raise RuntimeError("oh no, it happened")
        
def subprogram():
    print("this is subprogram, doing subprogram things")
    try:
        may_throw()
    except:
        print("oh no it failed")
    print("subprogram finished")
    
def mainprogram():
    print("this is the main program, doing main program things")
    print("calling subprogram")
    subprogram()
    print("mainprogram finished")
        
mainprogram()
def may_throw():
    raise RuntimeError("oh no, it happened")
        
def subprogram():
    print("this is subprogram, doing subprogram things")
    may_throw()
    print("subprogram finished")

    
def mainprogram():
    print("this is the main program, doing main program things")
    print("calling subprogram")
    subprogram()
    print("mainprogram finished")
    
try:        
    mainprogram()
except:
    print("oh no it failed")

Vlastní výjimky#

Běžnou praxí je implementace vlastních výjimek ušitých na míru problému. Aby výjimka fungovala jako výjimka, musí být potomkem třídy Exception. Do výjimek obvykle v konstruktoru předáváme informace důležité k interpretaci a identifikaci chyby, případně implementujeme ještě nějaké pomocné metody.

class MyException(Exception):
    def __init__(self, message, details):
        super().__init__(message)
        self.details = details
        
try:
    raise MyException("oh no", "here are the details of the `oh no` problem")
except MyException as e:
    print(e)
    print(e.details)
oh no
here are the details of the `oh no` problem

Trochu smysluplnější příklad:

class InvalidConfigurationError(Exception):
    def __init__(self, config_key, value, reason):
        super().__init__(f"Configuration key {config_key} has an unacceptable value {value}: {reason}")
        self.config_key = config_key
        self.value = value
        self.reason = reason
        
cfg = {
    "url": "www.mamradjogurt.cz"
}

def app(**cfg):
    raise InvalidConfigurationError("url", cfg["url"], "url does not exist")
    
try:
    app(**cfg)
except Exception as e:
    print(e)
Configuration key url has an unacceptable value www.mamradjogurt.cz: url does not exist

Dekorátor#

Dekorátor#

Návrhový vzor, detailně popsaný např. na webu refactoring.guru.

UML diagram dekorátoru, refactoring.guru

Zjednodušeně se dá říct, že dekorátor umožňuje opakovatelným způsobem rozšiřovat funkcionalitu existujícího kódu tak, že jej obalí dalším kódem (je to vlastně takový wrapper).

V pythonu mají dekorátory zvláštní postavení - máme k dispozici zjednodušující syntaxi pro jejich použití (jakýsi syntactic sugar).

Ukažme si to na jednoduché funkci.

def add(x, y):
    return x + y

add(1, 2)
3

Zkusme tuto funkci o něco rozšířit, např. o oznámení, že byla zavolaná, ale nesahejme na její definici. Jedna možnost, jak to udělat, je napsat wrapper - novou funkci, která tu původní obalí.

def wrapper(x, y):
    print("calling funcion add")
    return add(x, y)

wrapper(1, 2)
calling funcion add
3

Napišme si trochu obecnější wrapper: napišme funkci, která dostane obalovanou funkci na vstupu, a vrátí obalený výsledek. Jméno funkce najdeme pod atributem __name__.

def better_wrapper(func):
    def wrapper(x, y):
        print(f"calling function {func.__name__}")
        return func(x, y)
    return wrapper

wrapped_add = better_wrapper(add)
wrapped_add(1, 2)
calling function add
3

Teď můžeme obalit i jinou funkci.

def multiply(x, y):
    return x * y

wrapped_multiply = better_wrapper(multiply)
wrapped_multiply(1, 2)
calling function multiply
2

Jedinou slabinou je, že náš better_wrapper stále předpokládá, že obalovaná funkce přijímá dva argumenty. Můžeme napsat obecný wrapper. Vzhledem k tomu, co dělá, ho pojmenujme log

def log(func):
    def wrapper(*args, **kwargs):
        print(f"calling function {func.__name__}")
        return func(*args, **kwargs)
    return wrapper

Takový wrapper už umí obalit úplně libovolnou funkci.

def some_function(arg1, **kwargs):
    print(arg1, kwargs.keys())
    
logged_some_function = log(some_function)

logged_some_function(True, x=3)
calling function some_function
True dict_keys(['x'])

Funkce, která vrácí obalenou funkci, je vlastně dekorátorem (rozšiřuje funkcionalitu existujícího objektu). Pro komfort je možné v pythonu dekorovat funkce již při definici - nemusíme zavádět nová jména pro dekorované varianty. V pythonu k tomu slouží následující syntaxe

@log
def another_function():
    print("this function does not actually do anything")
    
another_function()
calling function another_function
this function does not actually do anything

Můžeme si dovolit ještě jednu úroveň abstrakce. Chování dekorátoru může být závislé na nějakém další parametru. Potřebujeme tedy napsat funkci, která nám vrátí dekorátor. Ale dekorátor je funkce, která vrací funkci. Takže napíšeme funkci, která vrací funkci, která vrací funkci.

Přidejme k našemu dekorátoru možnost logování vypnout.

def log(do_log):
    def dec(func):
        def wrapper(*args, **kwargs):
            if do_log:
                print(f"calling function {func.__name__}")
            return func(*args, **kwargs)
        return wrapper
    return dec

@log(True)
def add(x, y):
    return x + y

@log(False)
def multiply(x, y):
    return x * y

add(1, 2)
multiply(1, 2)
calling function add
2

Tedy funkce log vyrábí různé dekorátory v závislosti na tom, jaký argument jí předáme. Pokud je do_log==True, pak dostaneme dekorátor, který k dekorované funkci přidá print. Pokud do_log==False, dostaneme triviální dekorátor, který nic nedělá.

Takový argument pak může sídlit v nějaké globální proměnné reprezentující nastavení (to je samo o sobě trochu nevhodné, ale o tom jindy):

enable_logging = True


@log(enable_logging)
def add(x, y):
    return x + y

@log(enable_logging)
def multiply(x, y):
    return x * y

add(1, 2)
multiply(1, 2)
calling function add
calling function multiply
2

Příklady použití#

Zkusme mírně rozpracovat příklad s logováním: naše “decorator factory” teď bude záviset na globálne nastaveném log_level, podle kterého bude různě detailně zapisovat informace. Je to jen mírná modifikace předchozího příkladu.

LOG_INFO    = 0
LOG_WARNING = 1
LOG_DEBUG   = 2

LOG_STR_LST = ["INFO", "WARNING", "DEBUG"]

log_level = LOG_DEBUG

def log(level = LOG_INFO):
    def dec(func):
        def wrapper(*args, **kwargs):
            if level <= log_level:
                print("{}: running function: {}".format(LOG_STR_LST[level], func.__name__))
                if log_level >= LOG_DEBUG:
                    print("\targs:", args)
                    print("\tkwargs:", kwargs)
            return func(*args, **kwargs)
        return wrapper
    return dec

@log(LOG_INFO)
def add(x, y):
    return x + y

@log(LOG_WARNING)
def do_warning_level_stuff():
    pass

@log(LOG_DEBUG)
def do_debug_level_stuff(**kwargs):
    pass

do_debug_level_stuff(neco = True)
add(1, 2)
do_warning_level_stuff()
DEBUG: running function: do_debug_level_stuff
	args: ()
	kwargs: {'neco': True}
INFO: running function: add
	args: (1, 2)
	kwargs: {}
WARNING: running function: do_warning_level_stuff
	args: ()
	kwargs: {}

Měření času#

Dekorátor je poměrně milý způsob, jak snadno k funkci přidat měření času. Použijme k tomu balík time, pomocí kterého si pouze zaznamenámě čas před a po spuštění měřené funkce.

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        elapsed_time = end - start
        print(f"Elapsed time: {elapsed_time} seconds")
        return result
    return wrapper
    

Fibonacciho čísla a memoizace#

Naivní implementace výpočtu Fibonacciho čísel obvykle velmi rychle zaběhne do hluboké a široké rekurze, což je velmi pomalé. Demostrujme si to s použitím měřiče z předchozího příkladu.

def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)
    
timed_fibonacci = timer(fibonacci)

timed_fibonacci(38)
Elapsed time: 15.52717924118042 seconds
39088169

Jednou možností, jak výpočet zrychlit, je přepsání pomocí obyčejného for cyklu s využitím takzvané memoizace - tedy ukládání výsledků předchozích běhů. K výpočtu n-tého Fibonacciho čísla potřebujeme vždy dvě předchozí. Abychom je nemuseli počítat pořád znovu, můžeme si je prostě uložit.

@timer
def fibonacci_loop(n):
    a = 0
    b = 1
    for _ in range(1, n):
        a, b = b, a+b
    return b

fibonacci_loop(38)
Elapsed time: 3.337860107421875e-06 seconds
39088169

V případě Fibonacciho čísel je to už takhle celkem jednoduché, ale můžeme zkusit napsat obecnější memoizaci pomocí dekorátoru. Zjednodušme si to předpokladem, že dekorovaná funkce bude přijímat jediný argument:

def memoize(func):
    cache = {}
    def wrapper(n):
        if n in cache:
            return cache[n]
        result = func(n)
        cache[n] = result
        return result
    return wrapper


@memoize
def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

timed_cached_fibonacci = timer(fibonacci)
timed_cached_fibonacci(38)
Elapsed time: 3.314018249511719e-05 seconds
39088169

OOP#

Objektově orientované programování (OOP)#

Základní idea objektově orientované programování spočívá ve sdružení dat a relevantních funkcí do ucelených objektů. Objektem obvykle názýváme právě takovouto abstrakci, např. bankovní účet - objekt sdružující informace o účtu, jeho majiteli a operace na něm (účtu, nikoliv majiteli). Různé objekty pak obvykle popisujeme pomocí třídy/class. Funkce či procedury definované v třídě se nazývají metody třídy, data/proměnné nazýváme atributy.

Často se hovoří o tom, zda ten či onen jazyk podporuje OOP, čímž se obvykle myslí, zda v něm existuje právě něco jako class. Jsou to trochu oddělené koncepty. Třeba v jazyce C nic jako třída neexistuje, ale to neznamená, že se v něm nedá programovat objektově orientovaně. Právě naopak, obvykle se to dělá, ale některých aspektů OOP je těžší dosáhnou (např. dědičnosti).

Třída (class) je pouze jakousi definicí či specifikací dané abstrakce - objektu. Rozličuje pak její konkrétní realizace, nazývané instance. Rozpracujme příklad s bankovním účtem.

class BankAccount:
    pass

account1 = BankAccount()
account2 = BankAccount()

V proměnných account1 a account2 jsou dvě různé instance třídy BankAccount (často říkáme objekty typu BankAccount).

print(account1 is account2)
print(type(account1), type(account2))
False
<class '__main__.BankAccount'> <class '__main__.BankAccount'>

Taková třída je víceméně k ničemu - přidejme k ní nějaká data a metody. Instance třídy se obvykle vytvářejí pomocí funkce, která se nazývá konstruktor. Konstruktor má za úkol alokovat paměť pro třídu a jej atributy a případně je inicializovat, tj. přiřadit jim nějakou hodnotu předanou například jako argument konstruktoru.

V Pythonu je konstrukce instance oddělená od inicializace atributů. Na konstruktor, tedy funkci __new__ v podstatě nikdy nesáhneme, ale budeme často psát inicializátor - funkci __init__. V těle inicializátor definujeme atributy třídy a inicializujeme je. Navzdory tomuto faktickému rozdílu se o funkci __init__ běžné hovoří jako o konstruktoru.

class BankAccount:
    def __init__(self, name, balance, number):
        self.name = name
        self.balance = balance
        self.number = number
        
    def withdraw(self, amount):
        self.balance -= amount
        
    def deposit(self, amount):
        self.balance += amount
        
    def report(self):
        print(f"Ucet c. {self.number}, {self.name}, zustatek: {self.balance} Kc")

ucet = BankAccount("Vaclav", 127, 123456789)
ucet.deposit(400)
ucet.withdraw(200)
ucet.report()
print(ucet.balance)
Ucet c. 123456789, Vaclav, zustatek: 327 Kc
327

Každá metoda potřebuje mít referenci na samotný objekt - přistupuje pomocí ní k jednotlivým atributům. Python ji automaticky vkládá jako první argument do metod. Je nutné s tím ale s v signaturách funkcí počítat. Volání metody instance fakticky vypádá takto:

BankAccount.deposit(ucet, 400) # dela to same jako ucet.deposit(400)
ucet.report()
Ucet c. 123456789, Vaclav, zustatek: 727 Kc

V pythonu se tento první argument tradičně označuje self - tím tedy instance referuje sama k sobě. V jiných jazycích potkáte například označení this.

class vs instance attributes#

Atributy třídy (class attributes) jsou společné pro celou třídu a její instance. Atributy instance (instance attributes) jsou vázané na konkrétní instanci.

class BankAccount:
    pa_percent = 5
    
    def __init__(self, name, balance, number):
        self.name = name
        self.balance = balance
        self.number = number
        
    def withdraw(self, amount):
        self.balance -= amount
        
    def deposit(self, amount):
        self.balance += amount
        
    def report(self):
        print(f"Ucet c. {self.number}, {self.name}, zustatek: {self.balance} Kc")
        
    def earn_monthly_interest(self):
        self.balance *= (1 + (self.pa_percent / 100) ** (1/12))

ucet = BankAccount("Vaclav", 127, 123456789)
ucet.deposit(400)
ucet.withdraw(200)
ucet.earn_monthly_interest()
ucet.report()
Ucet c. 123456789, Vaclav, zustatek: 581.7584432338033 Kc
print(BankAccount.pa_percent)
# print(BankAccount.balance) # vyvola vyjimku
5

Magic methods (dunder methods)#

Dunder methods jsou jsou speciální metody, ktéré můžeme implementovat ve svých třídách a dodat jim tím chování, která je v pythonu v jistém smyslu stadardní. Např. voláme-li v pythonu len(x), interpret ve skutečnosti volá x.__len__(). Všechny dunder metody mají před a za názvem dvě podtržítka - odsud ostatně plyne jejich název: double underscore -> double under -> dunder. Kompletní výčet dunder metod najdeme v dokumentaci.

Jiným příkladem může být dunder metoda __add__, která se volá při vyhodnocení výrazů se sčítáním. Tedy x + y znamená x.__add__(y). Ukažme si implementaci obou na příkladu.

class Vector:
    def __init__(self, *elements):
        self.elements = elements
    
    def __len__(self):
        return len(self.elements)
    
    def __add__(self, other):
        if len(self) != len(other):
            raise ValueError("Vectors do not have the same length")
        new_elements = [x+y for x, y in zip(self.elements, other.elements)]
        return Vector(*new_elements)
    
a = Vector(1, 2)
b = Vector(2, 3)

c = a + b
print(c.elements, len(c))
(3, 5) 2

Zajímavým příkladem jsou metody __str__ a __repr__. Obě se používají pro konverzi objektu na string, ale každá s jiným záměrem. Zatímco __str__ může vést na jakoukoliv stringovou reprezentaci (evokováno voláním str(x)), __repr__ má produkovat string, který je strojově zpracovatelný. Implementujme tyto metody pro bankovní účet z úvodu.

class BankAccount:
    def __init__(self, name, balance, number):
        self.name = name
        self.balance = balance
        self.number = number
        
    def withdraw(self, amount):
        self.balance -= amount
        
    def deposit(self, amount):
        self.balance += amount
        
    def report(self):
        print(str(self))
        
    def __str__(self):
        return f"Ucet c. {self.number}, {self.name}, zustatek: {self.balance} Kc"
    
    def __repr__(self):
        return f"{self.__class__.__name__}('{self.name}', {self.balance}, {self.number})"
    
ucet = BankAccount("Vaclav", 200, 123456789)
ucet.deposit(50)
repstring = repr(ucet)
print(repstring)

ucet2 = eval(repstring)
print(ucet2)
BankAccount('Vaclav', 250, 123456789)
Ucet c. 123456789, Vaclav, zustatek: 250 Kc

Posledním příkladem může být třeba dunder __call__, který definuje na objektu operaci kulatá závorka. Udělejme z polynom z kapitoly o funkcích objekt.

class Polynomial:
    """Returns a callable Polynomial object."""
    def __init__(self, *coefs):
        self.coefs = coefs
        
    def __call__(self, x):
        val = self.coefs[-1]
        for c in reversed(self.coefs[:-1]):
            val = val * x + c
        return val
    
    def order(self):
        return len(self.coefs - 1)
import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(-3, 1, 201)
p = Polynomial(1, 2, 1)
y = p(x)

plt.plot(x, y)
plt.show()
../_images/23c4f4a3b29e3de915121797dc1296207e2b98cf6d6b56bb32039b751ae88f3b.png

public vs. private#

Např. v C++ rozlišujeme privátní a veřejné atributy (případně protected).

class BankAccount {
    public:
        int get_balance() { return balance; }
    private:
        int deposit;
};

BankAccount ucet = BankAccount();

printf("%d\n", ucet.get_balance()); // ok
printf("%s\n", ucet.balance); // big no no ~ Cannot access private member

V pythonu se private a public vůbec nerozlišuje:

We are all consenting adults. (Anyone can touch your privates.)

Občas se o tom vedou spory, ale dokumentace o tom hovoří celkem přímočaře:

class BankAccount:
    def __init__(self, balance):
        self.balance = balance
        
ucet = BankAccount(200)
ucet.balance = 100000 # tohle nechceme
print(ucet.balance)
100000

Konvence říká, že atributy s prefixem jednoho podtržíka se považuje za privátní. Ale python nic takového samozřejmě nevynucuje. Vývojová prostředí na to ale typicky upozorňují.

class BankAccount:
    def __init__(self, balance):
        self._balance = balance
        
ucet = BankAccount(200)
ucet._balance = 100000 # tohle nechceme
print(ucet._balance)
100000

Dokumentace zmiňuje dvojité podtržítko jako možnost, jak dosáhnout private chování, ale ve skutečnosti je zatím jiný mechanismus (name mangling) s jinou motivací (name clashes u dědičnosti).

class BankAccount:
    def __init__(self, balance):
        self.__balance = balance
        
ucet = BankAccount(200)
# ucet.__balance = 100000 # tohle nefunguje
ucet._BankAccount__balance = 100000 # tohle ale ano
print(ucet._BankAccount__balance)
100000

Osobní názor

Python nemá mechanismus pro rozlišování public/private a proto bychom neměli takové chování vynucovat. Atributy míněné jako private označme jedním podtržítkem. Pokud potřebujete normální public/private chování, použijte jiný jazyk.

Getter a setter#

Pomocí zabudovaných dekorátorů můžeme třídu doplnit o getter a setter - umožňující kontrolu nad přiřazováním a navracením (private) hodnot.

class Person:
    def __init__(self, name):
        self.name = name
        self._rc = None
    
    @property
    def rc(self):
        return self._rc
    
    @rc.setter
    def rc(self, cislo):
        if (cislo % 11) != 0:
            raise ValueError("Please enter a valid rodne cislo")
        self._rc = cislo
        
person = Person("Vaclav")
person.rc = 9904100129
print(person.rc)
9904100129

Statické metody, metody třídy a metody instance#

Metody, které jsme dosud viděli, jsou metody instance, tj. potřebují vazbu na konkrétní instanci (první argument je reference na instanci).

Kromě toho můžeme definovat metody třídy, které mají reference pouze na třídu (nikoliv instanci).

Statické metody fungují bez vazby na třídu či instanci. Necháváme je ve třídě, protože např. tematicky souvisí, ale ke své funkci fakticky referenci na třídu nepotřebují.

Statické metody či metody třídy se často používají třeba pro implimentaci více “konstruktorů”, protože v pythonu nemáme přetěžování funkcí.

import math

class Circle:
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

    @classmethod
    def from_diameter(cls, diameter):
        radius = diameter / 2
        return cls(radius)
    
    @classmethod
    def from_area(cls, area):
        radius = math.sqrt(area / math.pi)

    @staticmethod
    def is_valid(radius):
        return radius > 0
    
Circle.is_valid(-3)
False

Dataclass#

import json

class Config:
    def __init__(self, logfile, check_updates):
        self.logfile = logfile
        self.check_updates = check_updates
        
    def save_to_file(self, filename):
        d = {
            "logfile": self.logfile,
            "check_updates": self.check_updates
        }
        with open(filename, "w") as file:
            json.dump(d, file)
            
    @staticmethod
    def load_from_file(filename):
        with open(filename, "r") as file:
            data = json.load(file)
        return Config(**data)
        
        
cfg1 = Config("file.log", True)
cfg1.save_to_file("pokus.json")

cfg2 = Config.load_from_file("pokus.json")
print(cfg2.logfile)
file.log
from dataclasses import dataclass, asdict
import json


@dataclass
class Config:
    logfile: str
    check_updates: bool
    
    def save_to_file(self, filename):
        with open(filename, "w") as file:
            json.dump(asdict(self), file)
    
    @staticmethod
    def load_from_file(filename):
        with open(filename, "r") as file:
            data = json.load(file)
        return Config(**data)
    
        
cfg1 = Config("file.log", True)
cfg1.save_to_file("pokus.json")

cfg2 = Config.load_from_file("pokus.json")
print(cfg2)
Config(logfile='file.log', check_updates=True)

Iterátory a generátory#

Když iterujeme přes kolekci

lst = [1, 2, 3]

for x in lst:
    print(x)
1
2
3

python ve skutečnosti provádí něco takového:

lst = [1, 2, 3]

iter_obj = iter(lst)

while True:
    try:
        x = next(iter_obj)
        print(x)
    except StopIteration:
        break
1
2
3

Tedy v duchu Duck typing, iterátor je něco, co dostaneme, když zavoláme funkci iter, a můžeme na tom volat funkci next, dokud to nevyhodí výjimku StopIteration.

Když si ještě řekneme, že iter(obj) ve skutečnosti volá obj.__iter__() a next(obj) ve skutečnosti volá obj.__next__(), můžeme si napsat vlastní iterátor.

# vlastni iterator
class Iterator:
    def __init__(self, data):
        self.data = data
        self.index = 0
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.index == len(self.data):
            raise StopIteration
        self.index += 1
        return self.data[self.index - 1]
    
test = Iterator([1,2,3,5,6])

for i in test:
    print(i)
1
2
3
5
6
class Fibonacci:
    def __init__(self, n=10):
        self.curr = 1
        self.last = 0
        self.it = 1
        self.n = n
        
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.it > self.n:
            raise StopIteration
        self.it += 1
        
        ret = self.last
        self.last, self.curr = self.curr, self.curr + self.last
        return ret

for fib in Fibonacci(6):
    print(fib)
    
0
1
1
2
3
5
def fib(n):
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

print(type(fib(10)))
for i in fib(10):
    print(i)
<class 'generator'>
0
1
1
2
3
5
8
13
21
34

Dědičnost#

Dědičnost v objektově orientovaném programování umožňuje novým třídám převzít vlastnosti a chování od již existujících tříd. Hlavním účelem dědičnosti je zvýšení znovupoužitelnosti kódu a vytváření vztahů mezi třídami. Díky dědičnosti můžeme vytvářet sofistikovanější a organizovanější systémy bez zbytečné duplikace kódu, což vede k lepší správě a udržitelnosti softwarových projektů.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

class Employee(Person):
    def __init__(self, name, age, job_title, salary):
        super().__init__(name, age)
        self.job_title = job_title
        self.salary = salary
        
    def payout(self):
        print(f"paying {self.salary} to {self.name}")


class GoodEmployee(Employee):
    def payout(self):
        print(f"paying {self.salary + 10000} to {self.name}")        
        
person = Person("Václav Alt", 31)
employee = Employee("Jindřich Sádlo", 28, "instalatér", 45000)
good_employee = GoodEmployee("Viktor Hroutil", 29, "lepší instalatér", 45000)

# person.payout()
employee.payout()
print(person)
print(employee)
print(good_employee)
paying 45000 to Jindřich Sádlo
Name: Václav Alt, Age: 31
Name: Jindřich Sádlo, Age: 28
Name: Viktor Hroutil, Age: 29
def payout_employess(employees: list[Employee]):
    for employee in employees:
        employee.payout()
        
def send_annoying_mail(persons: list[Person]):
    for person in persons:
        print(f"sending annoying mail to {person.name}")
        
send_annoying_mail([person, employee])

try:
    payout_employess([person, employee])
except AttributeError as e:
    print(e)
    print("You can not payout an ordinary person")
sending annoying mail to Václav Alt
sending annoying mail to Jindřich Sádlo
'Person' object has no attribute 'payout'
You can not payout an ordinary person
def payout_employess(employees: list[Employee]):
    for employee in employees:
        if type(employee) == Employee:
            employee.payout()
        
def send_annoying_mail(persons: list[Person]):
    for person in persons:
        print(f"sending annoying mail to {person.name}")
        
send_annoying_mail([person, employee])
payout_employess([person, employee, good_employee])
sending annoying mail to Václav Alt
sending annoying mail to Jindřich Sádlo
paying 45000 to Jindřich Sádlo
def payout_employess(employees: list[Employee]):
    for employee in employees:
        if isinstance(employee, Employee):
            employee.payout()
        
def send_annoying_mail(persons: list[Person]):
    for person in persons:
        print(f"sending annoying mail to {person.name}")
        
send_annoying_mail([person, employee])
payout_employess([person, employee, good_employee])
sending annoying mail to Václav Alt
sending annoying mail to Jindřich Sádlo
paying 45000 to Jindřich Sádlo
paying 55000 to Viktor Hroutil

Method overriding (překrývání metod)#

Překrýváním se označuje to, když potomek třídy definuje vlastní implementaci některé metody předka. Může buď původní implementaci volat (např. přes konstrukci super()) a vykonat něco navíc, nebo může předchozí implementaci zcela nahradit. Např. v C++ je nutné metody označit klíčovým slovem virtual, aby bylo možné je překrýt, v Pythonu takový mechanismus není.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def __str__(self):
        return f"Name: {self.name}, Age: {self.age}"

class Employee(Person):
    def __init__(self, name, age, job_title, salary):
        super().__init__(name, age)
        self.job_title = job_title
        self.salary = salary
        
    def payout(self):
        print(f"paying {self.salary} to {self.name}")

    def __str__(self):
        person_info = super().__str__()
        return f"{person_info}, Job Title: {self.job_title}, Salary: {self.salary}"

person = Person("Václav Alt", 31)
print(person)

employee = Employee("Jindřich Sádlo", 28, "instalatér", 45000)
print(employee)
Name: Václav Alt, Age: 31
Name: Jindřich Sádlo, Age: 28, Job Title: instalatér, Salary: 45000

Vícenásobná dědičnost#

Python dovoluje vícenásobnou dědičnost dvojího druhu:

  1. do šířky (potomek má více rodičů)

  2. do hloubky (potomek může mít potomka)

Potomek podědí metody všech svých předků a může k nim přidat i nějaké vlastní.

class BatteryDevice:
    def __init__(self, capacity):
        self.capacity = capacity
        self.status = 100
    
    def get_status():
        return self.status
    
class TouchscreenDevice:
    def __init__(self, size):
        self.size = size
        
    def show_mesage(self, message):
        print(f"message on screen: {message}")
        

class Smartphone(BatteryDevice, TouchscreenDevice):
    def __init__(self, brand, capacity, size):
        BatteryDevice.__init__(self, capacity)
        TouchscreenDevice.__init__(self, size)
        self.brand = brand
        
    def check_battery_status(self):
        if self.status < 20:
            self.show_mesage("Warning: battery low")
    
phone = Smartphone("Samsung", 6000, 6.5)
phone.check_battery_status()
phone.status = 19
phone.check_battery_status()
message on screen: Warning: battery low

U více násobné dědičnosti je třeba dávat pozor na případy, kdy více rodičů implementuje tu samou metodu. Která varianta se pak zavolá?

class A:
    def do_something(self):
        print("this is A")
        
class B:
    def do_something(self):
        print("this is B")

class C(A, B):
    pass

c = C()
c.do_something()
this is A

Python volá tu metodu, která je první nařadě podle seznamu, kterému říká MRO - Method Resolution Order, který získává pomocí tzv. C3 linearizace, což je algoritmus, který je zcela mimo rozsah tohoto předmětu (zjednodušeně: zleva doprava, zdola nahoru). Důležité je, že na pořádí se můžeme podívat, MRO se spočítá během definice třídy a najdeme ho pod atributem __mro__

C.__mro__
(__main__.C, __main__.A, __main__.B, object)

Celá věc je poněkud matoucí. Následuje série podobných příkladů pouze s drobnými komentáři. Nejsnažší je si v konkrétních případech ověřit, že se třída chová, jak má, než se spoléhat na nějaký odhad.

Potřeba MRO je trochu patrnější, když přesuneme do konstruktoru. Následující příklad se chová stejně jako předchozí.

class A:
    def __init__(self):
        print("this is A")
        
class B:
    def __init__(self):
        print("this is B")

class C(A, B):
    pass

c = C()
this is A

Ale to znamená, že rodič B není inicializovaný, což je nežádoucí. Můžeme zavolat konstruktory explicitně, ale to taky není žádoucí, neboť mimo to zhoršuje udržitelnost kódu (hard-coded názvy) a může narušit Liskov substitution principle (to L v SOLID, o tom jindy).

class A:
    def __init__(self):
        print("this is A")
        
class B:
    def __init__(self):
        print("this is B")

class C(A, B):
    def __init__(self):
        A.__init__(self)
        B.__init__(self)
    
c = C()
this is A
this is B

Trochu lepší varianta je používat konstrukci super().__init__(). super() pracuje s MRO a zajišťuje, aby všechno bylo voláno jak má a aby nic nebylo voláno dvakrát, musíme ale super() volat ve všech zúčastněných třídách. (Všimněte si, že volání konstruktorů postupuje proti směru MRO - není moc těžké rozmyslet si proč).

class A:
    def __init__(self):
        super().__init__()
        print("this is A")
        
class B:
    def __init__(self):
        super().__init__()
        print("this is B")

class C(A, B):
    def __init__(self):
        super().__init__()
    
c = C()
this is B
this is A

Prevence vícenásobného volání se projeví zejména do diamond dědičnosti (někdy diamond of death, příčina zhouby v mnoha inheritance based projektech). Přestože třídy B a C dědí z A, konstruktor A se správně zavolá jen jednou.

class A:
    def __init__(self):
        print("this is A")
        
class B(A):
    def __init__(self):
        super().__init__()
        print("this is B")

class C(A):
    def __init__(self):
        super().__init__()
        print("this is C")

class D(B, C):
    def __init__(self):
        super().__init__()
        print("this is D")
        
d = D()
D.__mro__
this is A
this is C
this is B
this is D
(__main__.D, __main__.B, __main__.C, __main__.A, object)

Vidíme, že při explicitním volání se konstruktor A zavolá dvakrát.

class A:
    def __init__(self):
        print("this is A")
        
class B(A):
    def __init__(self):
        A.__init__(self)
        print("this is B")

class C(A):
    def __init__(self):
        A.__init__(self)
        print("this is C")

class D(B, C):
    def __init__(self):
        B.__init__(self)
        C.__init__(self)
        print("this is D")
        
d = D()
D.__mro__
this is A
this is B
this is A
this is C
this is D
(__main__.D, __main__.B, __main__.C, __main__.A, object)

Ohledně rozsáhlého využívání dědičnosti se vedou spory. Jsou tábory, které dědičnost zavrhují zcela, tábory které tvrdí, že bez dědičnosti nelze psát software a potom v podstatě všechny možné kombinace.

Osobní názor

Osobně se příkláním k postoji, že trocha dědičnosti je dobrá, ale pokud začnete řešit problémy spojené s Method Resolution Order, zašli jste příliš daleko a Váš program je špatně navržený.

Abstraktní třídy#

Smysl abstraktní třídy je definovat jakýsi společný interface pro tématicky spřízněné třídy. K abstraktní třídě není možné stvořit instanci, neboť nic neimplementuje (nebo alespoň ne všechno).

from abc import ABC, abstractmethod

class Shape(ABC):
    @abstractmethod
    def area(self):
        pass
    
    @abstractmethod
    def perimeter(self):
        pass
    
    
class Rectangle(Shape):
    def __init__(self, a, b):
        self.a = a
        self.b = b
    
    def area(self):
        return self.a * self.b
    
    def perimeter(self):
        return 2 * (self.a + self.b)
    
    
# shape = Shape() # nelze
rect = Rectangle(2, 3)
print(rect.area())
6

Python dovoluje dědění z více abstraktních tříd najednou.

class Building(ABC):
    @abstractmethod
    def can_i_get_in():
        pass
    
class Pentagon(Shape, Building):
    def __init__(self, a):
        self.a = a
        
    def area(self):
        return 4 # nechtelo se mi to hledat
    
    def perimeter(self):
        return 5 * self.a
    
    def can_i_get_in(self):
        return False
    
the_pentagon = Pentagon(3)
the_pentagon.can_i_get_in()
False

Porovnávání typů v kontextu dědičnosti#

Dosud jsme typy porovnávali pomocí zabudované funkce type, ale ta může být v kontextu potomků neodstatečná. Ukažme si dvě nové metody:

  1. isinstance(object, classinfo) ověří, zda object je instancí třídy classinfo nebo nějakého jejího potomka

  2. issubclass(class, classinfo) ověří, zda třída class je potomkem třídy classinfo

Následující příklad ilustruje, co to znamená oproti porovnávání type.

class A:
    pass

class B(A):
    pass

a = A()
b = B()
isinstance(b, B), issubclass(B, A), isinstance(b, A), type(b) == B, type(b) == A
(True, True, True, True, False)

Protocol (interface)#

Některé jazyky (např. Java) zavádějí rozhraní (interface) - jakýsi kontrakt (sadu metod) pro třídy, které jej implementují. Zároveň interface nemusí tuto implementaci poskytovat.

interface Drawable {
    void draw();
}

// Implement the interface in a class
class Circle implements Drawable {
    @Override
    public void draw() {
        System.out.println("Drawing a circle.");
    }
}

Jazyk C++ rozhraní nepodporuje a celou věc řeší pomocí abstraktních tříd.

#include <iostream>

// Define the interface
class Drawable {
public:
    virtual void draw() = 0;
};

// Implement the interface in a class
class Circle : public Drawable {
public:
    void draw() override {
        std::cout << "Drawing a circle." << std::endl;
    }
};

V Pythonu můžeme použít abstraktní třídy podobně jako C++, ale obvyklejší je použít Protocol. Python je dynamicky typovaný, takže můžeme využívat duck typing.

“If it looks like a duck and quacks like a duck, it’s a duck”

V následujícím příkladu třída Cabbage není potomkem třídy Drawable, ale pouze implementuje její metody.

from typing import Protocol

class Drawable(Protocol):
    def draw(self):
        ...
        
def draw_drawables(drawables: list[Drawable]):
    for d in drawables:
        d.draw()
        
class Cabbage:
    def draw(self):
        print(f"drawing {self.__class__.__name__}")
        
cabbage = Cabbage()

draw_drawables([cabbage])
drawing Cabbage

Vzpomeňte si např. na context manager nebo iterátor. Context manager je cokoliv, co implementuje metody __enter__ a __exit__, a iterátor cokoliv, co implementuje __iter__ a __next__.

Mixin#

Mixin je příkladem, jak lze elegantně využít vícenásobnou dědičnost. Mixin je třída, která poskytuje metody pro použití v jiných třídách, ale není určena k samostatnému použití.

Výhody:

  • jednoduché

  • separuje zodpovědnost

  • celkem flexibilní

Nevýhody:

  • kolize názvů a komplikace spojené s vícenásobnou dědičností (lze vyřešit kompozicí)

  • větší coupling mezi oddělenými třídami

class LoggerMixin:
    def log(self, message):
        print(f"logging: {message}")
        
class Database:
    def connect(self):
        pass
    
    def close(self):
        pass
    
class LoggedDatabase(LoggerMixin, Database):
    def connect(self):
        self.log("connecting to database")
        Database.connect(self)
    
    def close(self):
        self.log("closing database")
        Database.close(self)
        
dlb = LoggedDatabase()
dlb.connect()
dlb.close()
logging: connecting to database
logging: closing database
Composition vs. inheritance#

Osobně se mi vícenásobná dědičnost příčí a snažím se jí vyhnout, jak to jen jde. Oblíbený způsob je nahrazovat dědičnost kompozicí. Tedy místo toho, aby třída podědila vlastnosti několika jiných tříd, udržuje si referenci na instance, které potřebuje. Například:

class Logger:
    def __init__(self, log_strategies):
        self.log_strategies = log_strategies

    def log(self, message):
        for strategy in self.log_strategies:
            strategy.log(message)

class LogStrategy:
    def log(self, message):
        pass

class ConsoleLogStrategy(LogStrategy):
    def log(self, message):
        print(f"Console: {message}")

class FileLogStrategy(LogStrategy):
    def __init__(self, file_path):
        self.file_path = file_path

    def log(self, message):
        with open(self.file_path, "a") as log_file:
            log_file.write(f"File: {message}\n")

class DatabaseLogStrategy(LogStrategy):
    def log(self, message):
        # Code to connect and log message to a database
        pass

console_logger = Logger([ConsoleLogStrategy()])
console_and_file_logger = Logger([ConsoleLogStrategy(), FileLogStrategy("logfile.txt")])

console_logger.log("This is a console log message")
console_and_file_logger.log("This is a console and file log message")
Console: This is a console log message
Console: This is a console and file log message

Zvrácená dědičná implementace by mohl vypadat nějak takto:

class Logger:
    pass

class FileLogger(Logger):
    pass

class ConsoleLogger(Logger):
    pass

class FileAndConsoleLogger(FileLogger, ConsoleLogger):
    pass

Composition vs. inheritance#

Osobně se mi vícenásobná dědičnost příčí a snažím se jí vyhnout, jak to jen jde. Oblíbený způsob je nahrazovat dědičnost kompozicí. Tedy místo toho, aby třída podědila vlastnosti několika jiných tříd, udržuje si referenci na instance, které potřebuje. Například:

class Logger:
    def __init__(self, log_strategies):
        self.log_strategies = log_strategies

    def log(self, message):
        for strategy in self.log_strategies:
            strategy.log(message)

class LogStrategy:
    def log(self, message):
        pass

class ConsoleLogStrategy(LogStrategy):
    def log(self, message):
        print(f"Console: {message}")

class FileLogStrategy(LogStrategy):
    def __init__(self, file_path):
        self.file_path = file_path

    def log(self, message):
        with open(self.file_path, "a") as log_file:
            log_file.write(f"File: {message}\n")

class DatabaseLogStrategy(LogStrategy):
    def log(self, message):
        # Code to connect and log message to a database
        pass

console_logger = Logger([ConsoleLogStrategy()])
console_and_file_logger = Logger([ConsoleLogStrategy(), FileLogStrategy("logfile.txt")])

console_logger.log("This is a console log message")
console_and_file_logger.log("This is a console and file log message")
Console: This is a console log message
Console: This is a console and file log message

Zvrácená dědičná implementace by mohl vypadat nějak takto:

class Logger:
    pass

class FileLogger(Logger):
    pass

class ConsoleLogger(Logger):
    pass

class FileAndConsoleLogger(FileLogger, ConsoleLogger):
    pass

Numpy, matplotlib a pandas#

numpy - numerical python#

https://numpy.org/

Co o sobě říká numpy:

POWERFUL N-DIMENSIONAL ARRAYS Fast and versatile, the NumPy vectorization, indexing, and broadcasting concepts are the de-facto standards of array computing today.

NUMERICAL COMPUTING TOOLS NumPy offers comprehensive mathematical functions, random number generators, linear algebra routines, Fourier transforms, and more.

OPEN SOURCE Distributed under a liberal BSD license, NumPy is developed and maintained publicly on GitHub by a vibrant, responsive, and diverse community.

INTEROPERABLE NumPy supports a wide range of hardware and computing platforms, and plays well with distributed, GPU, and sparse array libraries.

PERFORMANT The core of NumPy is well-optimized C code. Enjoy the flexibility of Python with the speed of compiled code.

EASY TO USE NumPy’s high level syntax makes it accessible and productive for programmers from any background or experience level.

numpy arrays#

Základním stavebním kamenem jsou vícerozměrná pole. Oproti např. typu list mají pevnou velikost a mohou obsahovat prvky pouze jednoho typu. Vytváříme je pomocí zabudovaných funkci, jako je třeba celkem univerzální numpy.array nebo specializovaných, např. numpy.zeros, numpy.ones, numpy.linspace.

Pole mají řadu atributů. Mezi ty základní patří .shape, .size, .dtype. Jejich význam by mohl být zřejmá z následujících příkladů.

import numpy as np

a = np.array([1, 2, 3], dtype=np.double)
print(a)

a = np.zeros(shape=(3, 2), dtype=np.complex128)
print(a)

a = np.linspace(0, 1, 11)
print(a)

a = np.array(range(18), dtype=int).reshape(3, 6)
print(a)

a = np.array(range(8), dtype=int).reshape(2, 2, 2)
print(a)
[1. 2. 3.]
[[0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j]
 [0.+0.j 0.+0.j]]
[0.  0.1 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1. ]
[[ 0  1  2  3  4  5]
 [ 6  7  8  9 10 11]
 [12 13 14 15 16 17]]
[[[0 1]
  [2 3]]

 [[4 5]
  [6 7]]]

Mezi numpy poli jsou definovány všechny rozumné operace, včetně řady algebraických (násobení matic, skalární a vektorový součin, atd.). Řada z nich má asociovaný i nějaký operátor.

a = np.array([1, 2, 3])
b = np.array([1, 1, 1])

np.matmul(a, b) # a @ b # skalární součin / maticové násobení
6
c = 2 * a + b**2
print(c)
[3 5 7]
a * b # element-wise multiplication
array([1, 2, 3])

Broadcasting#

Broadcasting je způsob, jakým numpy řeší operace s poli různých velikostí. Nejsnáze se to ilustruje na násobení pole skalárem (číslem)

import numpy as np

a = np.array(range(10))
b = 2

c = a * b
print(c.shape, c)
(10,) [ 0  2  4  6  8 10 12 14 16 18]

numpy ‘natáhne’ číslo v proměnné b na velikost pole a a pak provede element-wise násobení.

Broadcasting se řídí dvěma jednoduchými pravidly. Porovnávájí se jednotlivé rozměry v .shape zprava doleva. Rozměry jsou kompatibilní, pokud:

  1. jsou shodné

  2. jeden z nich je 1.

Ukažme si to na několika příkladech.

import numpy as np

a = np.array(range(5)).reshape((5, 1))
b = a.transpose()

print(a.shape, b.shape)

a * b
(5, 1) (1, 5)
array([[ 0,  0,  0,  0,  0],
       [ 0,  1,  2,  3,  4],
       [ 0,  2,  4,  6,  8],
       [ 0,  3,  6,  9, 12],
       [ 0,  4,  8, 12, 16]])
# barevny obrazek velikosti IMG_SIZExIMG_SIZE. Kazdy barevny kanal chceme naskalovat zvlast

import numpy as np

IMG_SIZE = 4
im = np.array([1] * IMG_SIZE * IMG_SIZE * 3, dtype=int).reshape((IMG_SIZE, IMG_SIZE, 3))
scale = np.array([1, 2, 3])
scaled = im * scale
scaled
array([[[1, 2, 3],
        [1, 2, 3],
        [1, 2, 3],
        [1, 2, 3]],

       [[1, 2, 3],
        [1, 2, 3],
        [1, 2, 3],
        [1, 2, 3]],

       [[1, 2, 3],
        [1, 2, 3],
        [1, 2, 3],
        [1, 2, 3]],

       [[1, 2, 3],
        [1, 2, 3],
        [1, 2, 3],
        [1, 2, 3]]])

Pokud pole nemají brodcastable rozměry, numpy vyvolá výjimku:

Tip

Broadcasting je zpočátku trochu matoucí. Asi nejsnazší je vyzkoušet si pár jednoduchých příkladů. Brzy se Vám to dostane pod kůži.

Vektorizace#

Velkým přínosem jsou vektorizované operace. Vektorizací se rozumí konverze algoritmu z opakovaných aplikací operace na prvky kolekce po jednom do podoby, ve které se operace provede pro všechny prvky najednou (nebo alespoň pro více najednou). Samotná realizace vektorizace se může velmi lišit v závislosti na kontextu. Cílem je především vyšší efektivita.

numpy právě se svými poli umožňuje provádět mnoho operací vektorizovaně, což je zpravidla významně rychlejší.

Příklad#

vezměme seznam 10001 čísel a spočítejme jejich odmocninu a přičteme číslo 1, 4 různými způsoby:

  1. obyčejný for cyklus

  2. numpy-vektorizovaná operace

  3. hloupé nevyužití numpy pole

  4. dodatečná vektorizace původně nevektorizované operace

Čas změříme pomocí magic metod balíčku timeit.

import timeit
import numpy as np

def time_func(fun, number=1000, repeat=5):
    measurements = timeit.repeat(fun, number=number, repeat=repeat)
    mean = np.mean(measurements) * 1000 / number
    stdev = np.std(measurements) * 1000 / number
    print(f"function {fun.__name__} took ({mean:.3f}+-{stdev:.3f}) ms")
from math import sqrt
import numpy as np

N = 10001

def basic_op():
    xs = [(0.0 + i *0.01) for i in range(N)]
    ys = [sqrt(x) + 1 for x in xs]

def np_op():
    x = np.linspace(0., 1., N)
    y = np.sqrt(x) + 1

def np_dumb_op():
    x = np.linspace(0., 1., N)
    y = np.zeros(x.shape, dtype=np.double)
    for i, xi in enumerate(x):
        y[i] = sqrt(xi) + 1


def on_el_op(x):
    return sqrt(x) + 1

vectorized_op = np.vectorize(op)

def vectorized_basic_op():
    x = np.linspace(0., 1., N)
    y = vectorized_op(x)


time_func(basic_op)
time_func(np_op)
time_func(np_dumb_op)
time_func(vectorized_basic_op)
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/tmp/ipykernel_1080287/1644077815.py in <module>
     22     return sqrt(x) + 1
     23 
---> 24 vectorized_op = np.vectorize(op)
     25 
     26 def vectorized_basic_op():

NameError: name 'op' is not defined

matplotlib#

Rozsáhlá grafická knihovna určená pro tvorbu statických, animovaných či interaktivních vizualizací. My se zde omezíme pouze na podbalík pyplot, který slouží k tvorbě jednoduchých statických grafů.

Warning

Dokumentace k matplotlib není vždy zrovna nejčitelnější. Pokud se Vám podaří konečně podaří něco náročnějšího nastavit, pečlivě si to uschovejte, ať to za dva měsíce nemusíte celé procházet znovu.

Základní použití pomocí funkce plot.

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0.0, 2*np.pi, 100) # pole 100 hodnot od 0 do 2 pi, 
y = np.sin(x) # pole hodnot funkce sin(x) pro cisla z pole x

plt.plot(x, y)
plt.show()
../_images/83c914c076d91834db0ad136a22ea78c802fb4b4d174988001b9399888c4cc5a.png

Samotná funkce plot má celou řadu nastavení, viz dokumentace, která ovlivňují vzhled kreslených dat. Celkové vzezření grafu lze ovlivnit voláním funkcí přímo z balíku pyplot. Např:

import matplotlib.pyplot as plt
import numpy as np

x = np.linspace(0.0, 2*np.pi, 100)
y = np.sin(x)

plt.plot(x, y, "ro-", lw = 5.0, label="$\sin(x)$")
plt.title("Funkce sinus")
plt.xlabel("popis osy x")
plt.ylabel("popis osy y")
plt.ylim(-1.1, 1.1)
plt.xlim(0.0, 2*np.pi)
plt.legend()
plt.show()
../_images/f66c6ce56fdf3421cf39f79b1d6c46d88cf1a2160259bb36289137b5f0599fe3.png

figure a axes#

matplotlib celkem skrytě pracuje s konceptem dvou objektů: figure a axes. figure je celkový obrázek, který matplotlib vyprodukuje, zatímco axes jsou sada os, vůči kterým graf zakreslujeme. Jeden figure může obsahovat více axes, ale obráceně ne.

Existuje mnoho způsobů, jak tyto objekty vytvořit, v příkladech níže jsou dva nejpoužívanější.

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2 * np.pi, 200)

fig, axs = plt.subplots(1, 2, figsize=(8, 4))

axs[0].plot(x, np.sin(x))
axs[0].set_xlabel("x label")
axs[0].set_ylabel("y label")
axs[0].set_title("sinus")

axs[1].plot(x, np.cos(x))
axs[1].set_xlabel("x label")
axs[1].set_ylabel("y label")
axs[1].set_title("cosinus")

fig.tight_layout()

plt.show()
../_images/7ccfd0882580ee386d6c5dcc8685b9e24e61f4649766ed547015348a78a25485.png

Pozor na návratový typ funkce plt.subplots - závisí na počtu axes:

  • fig, axs = plt.subplots(1, 1) - axs je typu Axes

  • fig, axs = plt.subplots(1, 2) - axs je numpy.ndarray obsahující Axes

  • fig, axs = plt.subplots(2, 2) - axs je 2D numpy.ndarray obsahující Axes

Všimněte si také, že pokud přidáváte popisky apod. přes objekt Axes, mají metody prefix set_

import numpy as np
import matplotlib.pyplot as plt

x = np.linspace(0, 2*np.pi, 200)

fig = plt.figure(figsize=(6, 3))

ax = fig.add_subplot(121) # 121 značí 1 řádek, 2 sloupce, 1. z možných grafů
ax.plot(x, np.sin(x))

ax = fig.add_subplot(122) # 122 značí 1 řádek, 2 sloupce, 2. z možných grafů
ax.plot(x, np.cos(x))

plt.show()
../_images/f7712f8598a8a49d45bbba47a7bbb97ff534191e59134b5e8df629074d918177.png

Colormaps#

Z pohledu knihovny matplotlib je colormap něco, co umí převádí čísla z intervalu \([0, 1]\) na barvy formát RGBA, které reprezentuje. V matplotlibu je k disposici velké množství barevných map. Typické použité může vypadat takto:

import matplotlib.pyplot as plt
import numpy as np

cmap = plt.get_cmap("viridis")
cmap(0.5, alpha=0.8)
(0.127568, 0.566949, 0.550556, 0.8)
cmap(np.linspace(0, 1, 5), alpha=np.linspace(0, 1, 5))
array([[0.267004, 0.004874, 0.329415, 0.      ],
       [0.229739, 0.322361, 0.545706, 0.25    ],
       [0.127568, 0.566949, 0.550556, 0.5     ],
       [0.369214, 0.788888, 0.382914, 0.75    ],
       [0.993248, 0.906157, 0.143936, 1.      ]])
import matplotlib.pyplot as plt
import numpy as np

N = 100
x = np.linspace(0, 2 * np.pi, 100)
cmap = plt.get_cmap("plasma")
colors = cmap(np.linspace(0, 1, N), alpha=np.linspace(0.1, 1, N))

for i in range(N):
    plt.plot(x, (i+1)*np.sin(x) / N, color=colors[i])
plt.show()
../_images/def0c549819b4b6d9445b898c5fb2ff61d001cf4c1d9df513837d2f5270a1af5.png
Vlastní mapy#
import matplotlib.pyplot as plt
import numpy as np

N = 256
x = np.linspace(0, 2 * np.pi, 100)

top = plt.get_cmap("plasma", N // 2)
bottom = plt.get_cmap("viridis", N // 2)
colors = np.vstack((
    top(np.linspace(0, 1, N//2)),
    bottom(np.linspace(1, 0, N//2))
))

for i in range(N):
    plt.plot(x, (i+1)*np.sin(x) / N, color=colors[i])
plt.show()
../_images/f26069a16d26bdcc89f5be32648f305a0f837a2883539bcb4b3103ffc1ed773f.png
import matplotlib.pyplot as plt
import numpy as np

from matplotlib.colors import LinearSegmentedColormap

cdict = {
    'red': [
        [0.0,  1.0, 1.0],
        [0.5,  0.5, 0.5],
        [1.0,  0.5, 0.5]
    ],
    'green': [
        [0.0,  0.0, 0.0],
        [0.25, 0.0, 0.0],
        [0.75, 1.0, 1.0],
        [1.0,  1.0, 1.0]
    ],
    'blue': [
        [0.0,  0.0, 0.0],
        [0.5,  0.0, 0.0],
        [1.0,  1.0, 1.0]
    ]
}

N = 256
x = np.linspace(0, 2 * np.pi, 100)

cmap = LinearSegmentedColormap("MyCMap", segmentdata=cdict, N=N)

for i in range(N):
    plt.plot(x, (i+1)*np.sin(x) / N, color=cmap(i))
plt.show()
../_images/8eb0af82a87c0f50f4434d3d0133684c71f072ce2ed2cbc4180ebd298204bdca.png

Ticks#

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import MultipleLocator, AutoMinorLocator

x = np.linspace(0, 10, 500)
plt.plot(x, np.cos(x * np.pi))

ax = plt.gca()
ax.xaxis.set_major_locator(MultipleLocator(1))
ax.xaxis.set_major_formatter('{x:.0f} $\pi$')
ax.xaxis.set_minor_locator(AutoMinorLocator(2))
ax.xaxis.set_ticks_position('both')

ax.tick_params(axis='x', which="minor", width=1, length=5, color="r")
ax.tick_params(axis='x', which="major", width=3, length=5, color="g")

ax.tick_params(axis='y', which='both', left=False, right=False, labelleft=False)

ax.spines['left'].set_visible(False)
ax.spines['right'].set_visible(False)

plt.show()
../_images/d28e9fbad83e6d40633fe55f49ef186af6d914c49e9e93060cc76d384df8af39.png

Imshow#

import matplotlib.pyplot as plt
import numpy as np

SIZE = 256

x = np.linspace(0, 10., SIZE) - 5
y = np.linspace(0, 10., SIZE) - 5

xx, yy = np.meshgrid(x, y)
rr = np.sqrt(xx**2 + yy**2)
z = np.where(rr > 0, np.sin(np.pi * rr) / (np.pi * rr), 1.0)

plt.imshow(z, cmap="plasma")
<matplotlib.image.AxesImage at 0x7f3d76139270>
../_images/536e3306e7cb992407d92fe334ca132afadd0be28de5b696664b8f055d7e6b8b.png

Ukázky grafů různých typů#

Scatterplot#
import matplotlib.pyplot as plt
import numpy as np

data = {
    'a': np.arange(50),
    'c': np.random.randint(0, 50, 50),
    'd': np.random.randn(50)
}

data['b'] = data['a'] + 10 * np.random.randn(50)
data['d'] = np.abs(data['d']) * 100

plt.scatter('a', 'b', c='c', s='d', data=data)

plt.xlabel('entry a')
plt.ylabel('entry b')
plt.show()
../_images/863384716f13b5d6293765b16b0635d56248e70292c4854709cbcfc9108ba9be.png
Categorical plotting#
import matplotlib.pyplot as plt
import numpy as np

names = ['group_a', 'group_b', 'group_c']
values = [1, 10, 100]

plt.figure(figsize=(9, 3))

plt.subplot(131)
plt.bar(names, values)
plt.subplot(132)
plt.scatter(names, values)
plt.subplot(133)
plt.plot(names, values)
plt.suptitle('Categorical Plotting')
plt.show()
../_images/e0c3471b0288a0a6de3bc3fd0609557d0e83c9688944b1d10f7dfe28d695639f.png
Barplot#
import matplotlib.pyplot as plt
import numpy as np

strany = ["KSČM", "ANO", "TOP09", "Piráti", "ČSSD", "Zelení"]
barvy = ["red", "blue", "purple", "black", "orange", "green"]

np.random.seed(19680801)
hlasy = np.random.randint(0,150, len(strany))

plt.figure(figsize=(8,6))
plt.bar(strany, hlasy, color = barvy)
plt.title("Parlamentní volby 2021")
plt.ylabel("hlasy (tis.)")
Text(0, 0.5, 'hlasy (tis.)')
../_images/36d178d5eb2ab1b3b8c52e53d576c1d50d38b452b88dbf07a019ab6d7753b7f7.png
Boxplot#
import matplotlib.pyplot as plt
import numpy as np

# Fixing random state for reproducibility
np.random.seed(19680801)

# fake up some data
spread = np.random.rand(50) * 100
center = np.ones(25) * 50
flier_high = np.random.rand(10) * 100 + 100
flier_low = np.random.rand(10) * -100
data = np.concatenate((spread, center, flier_high, flier_low))
plt.plot(data)
plt.show()
../_images/78ef7e024cbcecba6adc8017a6de5564e56299e1ef7dd1d4b8b57f66fdb32ae3.png
import matplotlib.pyplot as plt
import numpy as np

fig1, ax1 = plt.subplots()

ax1.set_title('Basic Plot')
ax1.boxplot(
    [data, data[::3]],
    vert=False,
    notch=True,
    showfliers=False,
    meanline=True,
    showmeans=True
)

plt.show()
../_images/8cf047d8e2b9fd68aacfb6339e186753dfc79980221831e1713836095692bb41.png
Histogram#
import matplotlib.pyplot as plt
import numpy as np

N_points = 100000
n_bins = 20

# Generate a normal distribution, center at x=0 and y=5
x = np.random.randn(N_points)
y = 5*np.random.randn(100000) + 5

fig, axs = plt.subplots(1, 3, figsize=(9, 3), sharey=True, tight_layout=True)

# We can set the number of bins with the `bins` kwarg
axs[0].hist(x, bins=n_bins)
axs[1].hist(y, bins=n_bins)
axs[2].hist(y, bins=100)
plt.show()
../_images/930ec252a4d0869069a623acaeb5f778ca2d3f6046f5627c9ce57263e1ff77e5.png

PANDAS#

pandas is an open source, BSD-licensed library providing high-performance, easy-to-use data structures and data analysis tools for the Python programming language.

https://pandas.pydata.org/

Balík pandas je dnes prakticky standardem ve všech odvětvích, která mají co do činění s data science. Balík přináší do Pythonu koncep DataFrame objektů (jaký možná znáte např. z jazyka R), který usnadňuje práci s databázovými/tabulkovými data, přičemž prací se zde myslí vše od elementární statistiky až po komplexní filtrování a agregace společně s poctivou statistickou analýzou.

pandas je poměrně komplexní balík se spoustou funkcí, který se průběžně vyvíjí. V této kapitole proto naleznete jen základní přehled. Pro podrobnější studium doporučuji tutoriály přímo na webu projektu:

Základní principy#

Prvním stavebním kamenem je jednorozměrný objekt pd.Series. Je velmi podobný numpy polím - obsahuje hodnoty jednoho typu (libovolné python objekty) a je použitelný ve většině numpy funkcí. pd.Series má navíc k datům ještě značky (labels), obvykle označované jako index. Existuje nepřeberné množství způsobů, jak Series vytvářet.

import pandas as pd

s = pd.Series(range(5), index=["a", "b", "c", "d", "e"])
print(s)
a    0
b    1
c    2
d    3
e    4
dtype: int64
d = { l:i for i, l in enumerate("abcdefghijklmnopqrstuvwxyz", start=1)}
s = pd.Series(d)
s.head()
a    1
b    2
c    3
d    4
e    5
dtype: int64

pandas objekty podporují různé způsoby přístupu k datům. Klasická hranatá závorka je rezervovaná pro labels, běžné poziční indexování a slices fungují přes všudypřítomnou metodu .iloc.

print(s["b"])
print(s["x":])
print(s.iloc[:2])
print(s.iloc[[0, 13, -1]])
2
x    24
y    25
z    26
dtype: int64
a    1
b    2
dtype: int64
a     1
n    14
z    26
dtype: int64

Druhým a patrně nejčastěji užívaným kamenem je dvojrozměrný objekt pd.DataFrame. DataFrame je obecně tvořen sloupci obecně různých typů. Je užitečné na DataFrame nahlížet jako na slovník objektů pd.Series.

d = {"one": [1.0, 2.0, 3.0, 4.0], "two": [4, 3, 2, 1], "three": ["eins", "zwei", "drei", "vier"]}
df = pd.DataFrame(d, index=["a", "b", "c", "d"])
df
one two three
a 1.0 4 eins
b 2.0 3 zwei
c 3.0 2 drei
d 4.0 1 vier
df["one"]
df.loc[["a", "c"]]
s = df.loc["a"]
print(s, type(s), s.dtype)
one       1.0
two         4
three    eins
Name: a, dtype: object <class 'pandas.core.series.Series'> object
s = df[["one", "two"]].loc["a"] # prekvapiva nevyzadana konverze
print(s, type(s), s.dtype)
one    1.0
two    4.0
Name: a, dtype: float64 <class 'pandas.core.series.Series'> float64

DataFrame poskytuje i metodu postihující nějakou základní statistiku.

df.describe()
one two
count 4.000000 4.000000
mean 2.500000 2.500000
std 1.290994 1.290994
min 1.000000 1.000000
25% 1.750000 1.750000
50% 2.500000 2.500000
75% 3.250000 3.250000
max 4.000000 4.000000

Vybrané atributy objektu DataFrame

df.columns, df.index
(Index(['one', 'two', 'three'], dtype='object'),
 Index(['a', 'b', 'c', 'd'], dtype='object'))

IO#

Velká výhoda pandas je, že umí číst souobry řady formátů pod sjednoceným interfacem, jehož použití je přímočaré. Výsledkem je typicky pandas.Dataframe. Např. soubor formátu csv načteme pomocí funkce pandas.read_csv(). Plný výčet podporovaných formátu nalezneme v dokumentaci: https://pandas.pydata.org/docs/user_guide/io.html .

Čištění dat#

Načtená data je často třeba protřídit, zbavit nežádoucích hodnot, přejmenovat sloupce apod. Mezi nejčastější takové operace patří následující:

zahození, nahrazení nebo interpolace chybějících dat (NaN)#
df.dropna(inplace=True)
df['column_name'].fillna(0, inplace=True)
df['column_name'].interpolate(inplace=True)
odstranění duplicit#
df.drop_duplicates(inplace=True)
změna typu sloupce#
df['column_name'] = df['column_name'].astype('int')
přejmenování sloupců#
df.rename(columns={'old_name': 'new_name'}, inplace=True)
zahození sloupců#
df.drop(columns=['unwanted_column1', 'unwanted_column2'], inplace=True)
datetime#
df['date_column'] = pd.to_datetime(df['date_column'])
Filtrování a agregace#
mask1 = df['Age'] > 30
mask2 = df['Salary'] > 50000
filtered_df = df[mask1 & mask2]
df = pd.read_csv('https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv')
df.head()
PassengerId Survived Pclass Name Sex Age SibSp Parch Ticket Fare Cabin Embarked
0 1 0 3 Braund, Mr. Owen Harris male 22.0 1 0 A/5 21171 7.2500 NaN S
1 2 1 1 Cumings, Mrs. John Bradley (Florence Briggs Th... female 38.0 1 0 PC 17599 71.2833 C85 C
2 3 1 3 Heikkinen, Miss. Laina female 26.0 0 0 STON/O2. 3101282 7.9250 NaN S
3 4 1 1 Futrelle, Mrs. Jacques Heath (Lily May Peel) female 35.0 1 0 113803 53.1000 C123 S
4 5 0 3 Allen, Mr. William Henry male 35.0 0 0 373450 8.0500 NaN S
mask = df['Survived'] == 0
res = df[mask].groupby("Sex").agg({"Age": ["mean", "median"]})
res
Age
mean median
Sex
female 25.046875 24.5
male 31.618056 29.0
mask = df['Survived'] == 1
res = df[mask].groupby("Sex").agg({"Age": "mean"})
res
Age
Sex
female 28.847716
male 27.276022
df[df["Age"] < 15].count()
PassengerId    78
Survived       78
Pclass         78
Name           78
Sex            78
Age            78
SibSp          78
Parch          78
Ticket         78
Fare           78
Cabin          12
Embarked       78
dtype: int64
df["Fare"].agg(["mean", "sum", "median"])
mean         32.204208
sum       28693.949300
median       14.454200
Name: Fare, dtype: float64
df.hist("Age", bins=8, rwidth=0.8)
array([[<AxesSubplot:title={'center':'Age'}>]], dtype=object)
../_images/28d886ed97c251fba143ac1d9bb0a67b9923abd70ec6eef8ddefc17c2805dc6e.png
import matplotlib.pyplot as plt
plt.hist(df["Age"], bins=8, rwidth=0.8)
(array([ 64., 115., 230., 155.,  86.,  42.,  17.,   5.]),
 array([ 0.42  , 10.3675, 20.315 , 30.2625, 40.21  , 50.1575, 60.105 ,
        70.0525, 80.    ]),
 <BarContainer object of 8 artists>)
../_images/8eeacbcba62d994b4ec0ba3888a63ff2aef188363f636c51a9895bee80ab1e75.png

Covid data#

https://onemocneni-aktualne.mzcr.cz/covid-19

https://onemocneni-aktualne.mzcr.cz/api/v2/covid-19/nakazeni-vyleceni-umrti-testy.csv

import pandas as pd

df = pd.read_csv("../../data/nakazeni-vyleceni-umrti-testy.csv")

df.set_index(["datum"], drop = True, inplace = True)
df.index = pd.to_datetime(df.index)

new_names = ["inf", "cur", "dead", "tests", "ag", "d_inf", "d_cur", "d_dead", "d_tests", "d_ag"]
df.columns = new_names

df["d_inf"].plot(color="red", ls=":")
df.resample("7D").mean()["d_inf"].plot(color = "yellow", lw = 4.0)


df2 = df.resample("7D").agg({"d_inf" : "mean", "inf" : "sum"})
../_images/6157f3cca352e92ef3bd536cab96964d2eb9d9f1af6d2203313b31ef8bb8a6e9.png
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

fig, axs = plt.subplots(1, 2)

df["inf"].plot(ax=axs[0])
df["dead"].plot(ax=axs[1])
<AxesSubplot:xlabel='datum'>
../_images/10db1676e0a9c50bef3494eda69a991570cec0dbc8a32796f4e2a91cef15f711.png

HTTP API#

  • API: application programming interface

  • HTTP: hyper text transfer protocol

  • REST: representational state transfer What is a REST API

Nás zajimá: RESTful HTTP API

Jako RESTful HTTP API se obykle označuje rozhraní operující přes HTTP, které obvykle poskytuje nějaká data ve formě JSON.

ZDE je seznam veřejných REST API, která nevyžadují žádnou autentizaci.

Kromě toho můžeme využívat API tří meteostanic patřících Unicornu:

HTTP a balík requests#

Ke komunikaci prostřednictvím HTTP můžeme v Pythonu použít například balík requests. Použití lze shrnout do 3 kroků:

  1. sestavení požadavku - tj. poskládání URL podle instrukcí z dokumentace k danému API. U jednodušších API se jedná pouze o adresu, u složitějších lze předávat nějaké doplňující parametery, typicky metodou GET (parametry se vepisují přímo do adresy). U API vyžadujících autentizaci je obvykle nutné sestavit ještě hlavičku (header) HTTP požadavku.

  2. odeslání požadavku serveru (+ případná doplňující komunikace

  3. zpracování výsledků

Součástí HTTP komunikace je tzv. status code, číselný kód reprezentující stav komunikace. Asi nejčastější hodnoty jsou:

  • 200: OK

  • 400: Bad request

  • 401: Unauthorized

  • 403: Forbidden

  • 404: Not found

Kompletní seznam lze nalézt např. na wiki.

Ukažme si použití na API https://dog.ceo/api/breeds/image/random, které nám zprostředkuje náhodný obrázek psa.

import requests
import json

url = "https://dog.ceo/api/breeds/image/random"

r = requests.get(url) # odeslání požadavku metodou get

if r.status_code == 200: # 200 - OK, tj. má smysl zpracovat data
    data = r.json()      # 
    print(json.dumps(data, indent=4)) # data pouze hezky vytiskneme
{
    "message": "https://images.dog.ceo/breeds/spaniel-japanese/n02085782_2128.jpg",
    "status": "success"
}

Součástí data, která server vrací je adresa obrázku se psem. Použijeme pomocné funkce prostředí Jupyter, abychom si obrázek zobrazili.

import requests
from IPython.display import Image, display

url = "https://dog.ceo/api/breeds/image/random"
r = requests.get(url)

if r.status_code == 200:
    data = r.json()
    image_url = data["message"]
    display(Image(image_url))
../_images/0ed0e6f5101da8574bf203d2b1f85ad5dcc154fe430a2ef8dbc1fe2c7573ae11.jpg

Podobný příklad s komiksem XKCD:

import requests
from IPython.display import Image, display
url = "http://xkcd.com/info.0.json"
r = requests.get(url)
if r.status_code == 200:
    data = r.json()
    img_url = data["img"]
    display(Image(img_url))
../_images/8b2204c991c80a63f94488ba3ade31c1ad329552d6df3152d18b916575e2f39b.png

API s parametry#

Asi nejjednodušší způsob, jak předat parametry, je využít přímo metodu requests.get, konkrétně její argument params, jenž očekává slovník parametrů, který má k požadavku připojit.

Zkusme to u API, které vrací seznam univerzit v zemi, předané v parametru country.

import requests
url = "http://universities.hipolabs.com/search"

r = requests.get(url, params={"country": "Czech Republic"})

if r.status_code == 200:
    data = r.json()
    for uni in data:
        print(uni["name"])
Anglo-American University
Charles University Prague
Czech Technical University of Prague
Czech University of Agriculture Prague
Academy of Performing Arts, Film and TV Fakulty
University of South Bohemia
Mendel University of Agriculture and Forestry
Prague International University
Masaryk University
University of Ostrava
Prague College
Silesian University
Tiffin University Prague
University of Jan Evangelista Purkyne
University of Northern Virginia, Prague Campus
University of New York in Prague
University of Pardubice
Palacký University Olomouc
Tomas Bata University in Zlin
University of Veterinary and Pharmaceutical Science
Technical University of Mining and Metallurgy Ostrava
Prague Institute of Chemical Technology
Cevro Institut College
University of Economics Prague
Technical University of Liberec
University of Education Hradec Kralove
Brno University of Technology
University of West Bohemia
Střední průmyslová škola Mladá Boleslav
Anglo-American University
Charles University Prague
Czech Technical University of Prague
Czech University of Agriculture Prague
Academy of Performing Arts, Film and TV Fakulty
University of South Bohemia
Mendel University of Agriculture and Forestry
Prague International University
Masaryk University
University of Ostrava
Prague College
Silesian University
Tiffin University Prague
University of Jan Evangelista Purkyne
University of Northern Virginia, Prague Campus
University of New York in Prague
University of Pardubice
Palacký University Olomouc
Tomas Bata University in Zlin
University of Veterinary and Pharmaceutical Science
Technical University of Mining and Metallurgy Ostrava
Prague Institute of Chemical Technology
Cevro Institut College
University of Economics Prague
Technical University of Liberec
University of Education Hradec Kralove
Brno University of Technology
University of West Bohemia
Střední průmyslová škola Mladá Boleslav

Cvičení 1#

Napište program, který z JokeAPI stáhne vtip o programování, který nebude nevhodný a bude složen ze dvou částí (setup a delivery). Nejprve vypíše setup část vtipu a po nějaké prodlevě (time.sleep) vypíše zbytek, tj. delivery.

Dokumentace: https://sv443.net/jokeapi/v2/

import time

url = "https://sv443.net/jokeapi/v2/joke/Programming?blacklistFlags=nsfw,religious,political,racist,sexist"

params = {
    "blacklistFlags": "nsfw,religious,political,racist,sexist",
    "type": "twopart"
}

r = requests.get(url, params=params)
if r.status_code == 200:
    data = r.json()

    print(data["setup"])
    time.sleep(3)
    print(data["delivery"])
How do you generate a random string?
Put a Windows user in front of Vim and tell them to exit.

Cvičení 2#

Napište program, který bude v pravidelných intervalech sledovat rychlost větru z Unicorních meteostanic a bude je vykreslovat do grafu.

url1 = "https://uuappg01-eu-w-1.plus4u.net/ucl-weatherstation-maing01/ecdf51f870ae48b69e10fcd6cc2c7ef3/weatherConditions/getLast?code=PETROVKA&type=1"
url2 = "https://uuappg01-eu-w-1.plus4u.net/ucl-weatherstation-maing01/590f7ec72ad34126a5650fdfd807b3b6/weatherConditions/getLast?code=STRACHOVICE&type=1"
url3 = "https://uuappg01-eu-w-1.plus4u.net/ucl-weatherstation-maing01/36edc26c55a345798f3642ba8d22d80b/weatherConditions/getLast?code=MELTEMI&type=1"

import requests
import json
r = requests.get(url1)

if r.status_code == 200:
    data = r.json()

print(json.dumps(data, indent=4))
{
    "currentConditions": {
        "type1": {
            "did": "001D0A7100F3",
            "timestamp": "2023-03-17T10:14:33.000Z",
            "lsid": 575934,
            "type": 1,
            "txid": 1,
            "temp": 0.8,
            "hum": 47.9,
            "dewPoint": -9.1,
            "wetBulb": -3.8,
            "heatIndex": 0.2,
            "windChill": -5.5,
            "thwIndex": -6.1,
            "thswIndex": -4.5,
            "windSpeedLast": 25.74944,
            "windDirLast": 271,
            "windSpeedAvgLast1Min": 30.979795,
            "windDirScalarAvgLast1Min": 272,
            "windSpeedAvgLast2Min": 33.2811512,
            "windDirScalarAvgLast2Min": 272,
            "windSpeedHiLast2Min": 38.62416,
            "windDirAtHiSpeedLast2Min": 271,
            "windSpeedAvgLast10Min": 30.3682458,
            "windDirScalarAvgLast10Min": 272,
            "windSpeedHiLast10Min": 45.06152,
            "windDirAtHiSpeedLast10Min": 271,
            "rainSize": 2,
            "rainRateLast": 0,
            "rainRateHi": 0,
            "rainfallLast15Min": 0,
            "rainRateHiLast15Min": 0,
            "rainfallLast60Min": 0,
            "rainfallLast24Hr": 2,
            "rainStorm": 4,
            "solarRad": 438,
            "uvIndex": 3,
            "rxState": 0,
            "transBatteryFlag": 0,
            "rainfallDaily": 0,
            "rainfallMonthly": 298,
            "rainfallYear": 5129,
            "rainStormLast": 52,
            "rainStormStartAt": "2023-03-16T08:49:01.000Z",
            "rainStormLastStartAt": "2023-03-13T23:50:00.000Z",
            "rainStormLastEndAt": "2023-03-15T19:01:00.000Z",
            "awid": "ecdf51f870ae48b69e10fcd6cc2c7ef3",
            "gateway": "PETROVKA",
            "sys": {
                "cts": "2023-03-17T10:19:34.051Z",
                "mts": "2023-03-17T10:19:34.051Z",
                "rev": 0
            },
            "id": "64143eb619597c00407b4753"
        },
        "type2": null,
        "type3": {
            "did": "001D0A7100F3",
            "timestamp": "2023-03-17T10:14:33.000Z",
            "lsid": 575932,
            "type": 3,
            "barSeaLevel": 76.15936,
            "barTrend": 0.03556,
            "barAbsolute": 65.28562,
            "awid": "ecdf51f870ae48b69e10fcd6cc2c7ef3",
            "gateway": "PETROVKA",
            "sys": {
                "cts": "2023-03-17T10:19:34.152Z",
                "mts": "2023-03-17T10:19:34.152Z",
                "rev": 0
            },
            "id": "64143eb619597c00407b4759"
        },
        "type4": {
            "did": "001D0A7100F3",
            "timestamp": "2023-03-17T10:14:33.000Z",
            "lsid": 575933,
            "type": 4,
            "tempIn": 13.5,
            "humIn": 34.6,
            "dewPointIn": -1.8,
            "heatIndexIn": 12.1,
            "awid": "ecdf51f870ae48b69e10fcd6cc2c7ef3",
            "gateway": "PETROVKA",
            "sys": {
                "cts": "2023-03-17T10:19:34.103Z",
                "mts": "2023-03-17T10:19:34.103Z",
                "rev": 0
            },
            "id": "64143eb619597c00407b4756"
        }
    },
    "gateway": {
        "code": "PETROVKA",
        "name": "<uu5string/><UU5.Bricks.Lsi><UU5.Bricks.Lsi.Item language=\"en\">Peter's Chalets</UU5.Bricks.Lsi.Item><UU5.Bricks.Lsi.Item language=\"cs\">Petrovy boudy</UU5.Bricks.Lsi.Item></UU5.Bricks.Lsi>",
        "desc": "",
        "icon": "uubml-house",
        "additionalInformation": {
            "address": "Krkono\u0161e",
            "web": "https://petrovyboudy.cz",
            "phone": "",
            "gps": "50.7725608, 15.6123086"
        },
        "status": "online",
        "config": {
            "moduleTimers": {
                "heartbeat": 1800,
                "config": 10800,
                "weatherConditions": {
                    "get": 300,
                    "send": 600
                }
            }
        },
        "lastActivity": "2023-03-27T09:50:01.969Z"
    },
    "todayWeather": {
        "loTemp": null,
        "hiTemp": null,
        "loTimestamp": null,
        "hiTimestamp": null,
        "hiWindSpeed": null,
        "hiWindSpeedTimestamp": null
    },
    "uuAppErrorMap": {
        "ucl-weatherstation-main/weatherConditions/getLast/unsupportedKeys": {
            "type": "warning",
            "message": "DtoIn contains unsupported keys.",
            "paramMap": {
                "unsupportedKeyList": [
                    "$.type"
                ]
            }
        }
    }
}

Web scraping#

Základní pojmy#

Web scraping je speciálním případem obecnějšího pojmu - data scraping. Podle Wiki je deta scraping proces extrakce dat z human-readable výstupu z jiného programu. Web scraping je zřejmě zaměřen na sběr dat prezentovaných formou webové stránky.

Důležitým rozdílem oproti obyčejnému parsování (později) je právě ten fakt, že u scrapování zpracováváme data, která už byla určena přímo pro člověka. To v praxi znamená, že formát není nijak standardizován, není konzistentní nebo pravidelný. Z těchto důvodů bývá scraping protivný a zpravidla se k němu uchylujeme pouze v případě, kdy lepší cesta neexistuje. Důvodů může být mnoho - neochota druhé strany poskytnou použitelné API, přístupné ale nefunkční API a tak podobně. Typickou charakteristikou scrapování bývá to, že pro člověka je extrakce dat na pohled triviální, zatímco pro stroj/program může být velmi pracná. Ale už u velmi malých objemů dat bude člověk oproti stroji prohrávat.

Oproti mnoha různým zdrojům dat, web scraping je ještě poměrně přívětivý. Webovou stránku typicky poměrně snadno stáhneme a většina úsilí se přesouvá k parsování.

DOM#

DOM, neboli Document Object Model je standardizovaný, na jazykce nezávislý interface, který reprezentuje dokument. Pro nás bude důležité, že DOM reprezentuje dokument jako strom (tj. graf bez cyklů) - tedy jako datovou strukturu složenou z uzlů (nodes). Každý uzel má atributy a obsah, může mít i jednoho nebo více potomků (children) - v tom případě se takový uzel nazývá rodič (parent). Potomci jednoho rodiče jsou sourozenci (siblings). Právě tato struktura nám umožní elegantní procházení obsahu webové stránky.

HTML#

HTML, celým jménem HyperText Markup Language, který je primárně určený pro reprezentaci struktury webových stránek. HTML je statické a jako takové ho nelze přímo měnit. Důležité ale je, že pomocí HTML parserů lze HTML dokument přímo převést do DOM reprezentace, která už změny dovoluje (např. pomocí JavaScriptu).

HTML k reprezentaci struktury používá tzv. tagy dvojího druhu - párové a nepárové. Párové tagy jsou tvoří opening a closing tag, které obestupují obsah (content), nepárové tagy jsou standalone. Každý tag může mít nějaké doplňující atributy. Mezi obyklé patří atributy “id” a “class”, které se používají k identifikaci prvků v dokumentu, ať už pro účely změny jejich vzhledu (CSS) nebo pro přístup z kódu (opět např. JavaScript).

Následující příklad ukazuje velmi jednoduchou html stránku, ve které je řada párových i nepárových tagů.

<html>
    <head>
        <title>A simple web page</title>
    </head>
    <body>
        <h1>Toto je nadpis první úrovně.</h1>

        <ul class="ugly-ul">
            <li>první položka nečíslovaného seznamu</li>
            <li>druhá položka nečíslovaného seznamu</li>
        </ul>

        <p>Toto je odstavec obsahující hypertextový odkaz vedoucí na <a href="google.com">Google</a>.</p>
        <p>Po oddělovací čáře bude následovat obrázek</p>
        <hr />
        <img src="path/to/some/image" />
        <table id="table-we-want">
            <tr>
                <th>První sloupec</th>
                <th>Druhý sloupec</th>
                <th>Třetí sloupec</th>
            </tr>
            <tr>
                <td>1</td>
                <td>2</td>
                <td>3</td>
            </tr>
        </table>
    </body>
</html>
Parser#

Je nástroj, který vstupní data, nejčastěji text, převádí do nějaké datové struktury (například parse tree, syntax tree atd).

Příbuzný nástroj (někdy součástí parseru - hranice je mlhavá) je lexical analyzer, lexer. Lexer dělává první krok potřebný k parsování - bere vstupní text a reozebírá ho na tokeny. Ty potom žere právě parser.

Beautiful soup#

Beautiful soup je patrně nejpoužívanější nástroj na tahání dat z html a xml souborů. Dobře navrženo, skvěle popsáno a jak říká sama dokumentace:

It commonly saves programmers hours or days of work.

Instalace

pip3 install bs4

Ke správnému chodu Beautiful soup bude třeba nainstalovat ještě parser. Je více možností, každý má nějaké výhody a nevýhody. Na většinu práce plně vystačí lxml, což je Pythoní interface pro libxml - knihovnu napsanou v C. Je to rychlé a standardizované.

pip3 installlxml
from bs4 import BeautifulSoup

with open("data/sample.html") as fp:
    # soup = BeautifulSoup(fp, 'html.parser') # zabudovan7 pythoni parser
    soup = BeautifulSoup(fp, 'lxml') # zabudovan7 pythoni parser
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
/tmp/ipykernel_1080348/1232595439.py in <module>
      1 from bs4 import BeautifulSoup
      2 
----> 3 with open("data/sample.html") as fp:
      4     # soup = BeautifulSoup(fp, 'html.parser') # zabudovan7 pythoni parser
      5     soup = BeautifulSoup(fp, 'lxml') # zabudovan7 pythoni parser

FileNotFoundError: [Errno 2] No such file or directory: 'data/sample.html'
Objekty#

Většinu času budeme pracovat se třemi objekty: BeautifulSoup, Tag, NavigatableString.

title = soup.head.title
print(title, type(title), title.contents, title.name)
title = soup.head.title.string
print(title, type(title))
soup.head.title.string.replace_with("klobasa")
print(soup)
Atributy#
table = soup.body.table
table.attrs

table["id"]
table["class"]
webpage = """
<html>
    <head>
        <title>A simple web page</title>
    </head>
    <body>
        <h1>Toto je nadpis první úrovně.</h1>

        <ul class="ugly-ul">
            <li>první položka nečíslovaného seznamu</li>
            <li>druhá položka nečíslovaného seznamu</li>
        </ul>

        <p>Toto je odstavec obsahující hypertextový odkaz vedoucí na <a href="google.com">Google</a>.</p>
        <p>Po oddělovací čáře bude následovat obrázek</p>
        <hr />
        <img src="path/to/some/image" />
        <table id="table-we-want">
            <tr>
                <th>První sloupec</th>
                <th>Druhý sloupec</th>
                <th>Třetí sloupec</th>
            </tr>
            <tr>
                <td>1</td>
                <td>2</td>
                <td>3</td>
            </tr>
            <tr>
                <td><a href="irozhlas.cz">iRozhlas</a></td>
                <td><a href="hn.cz">Hospodářské noviny</a></td>
                <td><a href="idnes.cz">idnes.cz</a></td>
            </tr>
        </table>
    </body>
</html>
"""

soup = BeautifulSoup(webpage, "lxml")
img = soup.body.img
# a = soup.body.p
img["src"]
a = soup.body.p.a
a["href"]
Vyhledávání#
  • tag

  • id

  • funkce

  • regexp

soup.find_all("td")
soup.find_all(id="table-we-want")
def identify_link(tag):
    return str(tag.string) == "Google"

soup.find_all(identify_link)
import re

rec = re.compile("nadpis")
print(rec)
soup.find_all(rec)

Příklady#

Teploty#
for child in a.children:
    print(child)

print(a.caption)
for td in soup.find_all('td'):
    print(td.string)
soup.find(id="sample_table")
import requests
import bs4
import datetime

r = requests.get("http://vaclav-alt.github.io/data/index.html")
soup = bs4.BeautifulSoup(r.content)

def proc_table(table: bs4.Tag, dates: list, temps: list):
    date = table.caption.string
    for row in table.find_all("tr"):
        cols = row.find_all("td")
        dates.append(date + " " + str(cols[0].string))
        temps.append(cols[1].string)
    

dates = []
temps = []
for table in soup.find_all("table"):
    proc_table(table, dates, temps)

dates = [datetime.datetime.strptime(date, "%Y-%m-%d %H:%M:%S") for date in dates]
df = pd.DataFrame({
    "date" : dates,
    "temps" : temps
})
df = df.set_index("date", drop=True).astype(float)
df.plot()

Scrapethissite#

https://www.scrapethissite.com/

import requests
url = "https://www.scrapethissite.com/pages/simple/"

response = requests.get(url)
if response.status_code == 200:
    web = response.content
soup = BeautifulSoup(web, "lxml")

def this_is_the_one(tag):
    if tag.has_attr("class"):
        return "country" in tag["class"]
    return False

res = soup.find_all(this_is_the_one)
for div in res:
    print(list(div.h3.stripped_strings)[0])
soup = BeautifulSoup(web, "lxml")

def this_is_the_one(tag):
    if tag.has_attr("class"):
        return "country" in tag["class"]
    return False

res = soup.find_all(this_is_the_one)

data = []
for div in res:
    country = div.h3.contents[-1].strip()
    population = int(div.find_all(class_="country-population")[0].string)
    area = float(div.find_all(class_="country-area")[0].string)
    data.append({"country": country, "population": population, "area": area})
import pandas as pd

df = pd.DataFrame(data)
df["density"] = df["population"] / df["area"]
df = df.dropna()
df1.head()
import plotly.express as px
fig = px.scatter(df, x="area", y="population", color="density", size="density", size_max=100, hover_data=['country'])
fig.show()

pandas#

import pandas as pd

web = pd.read_html("http://vaclav-alt.github.io/data/index.html")
table = web[0]

regex#

  • naznak, jak to funguje

  • jak se to pouziva

  • priklad

  • proc nepouzivat pro html

    • ten SO post o html a regex

zminit
co delat s dynamickymi strankami (chatgpt)

ROBOTS

Development#

Scope#

Slovo “scope” se do češtiny překládá trochu obtížné, nejspíše by se dalo říct “obor platnosti”. Obvykle se i v češtině používá anglická varianta.

Pojem scope označuje oblast v programu, ve které je daný název nebo proměnná dostupná, známá.

Proměnné, funkce a objekty definované na nejširším možném scope (outermost, top-level, tedy mimo nevnořené do jiných definic) se nazývají globální a jsou přístupné v celém modulu i všude, kam modul importujeme. Proměnné, funkce a objekty definované v těle funkce, metody nebo třídy se nazývají lokální. Ty jsou dostupné pouze v bloku, ve kterém jsou definované. O příslušných oblastech pak mluvíme jako o lokálním či globálním scope.

a = 1 # globální proměnná

def f(x):
    return x + a # ke globální proměnné můžeme referovat i v lokálním scope

f(1)
2
def f(x):
    b = 1 # lokální proměnná
    return x + b # ke globální proměnné můžeme referovat i v lokálním scope

# b # skončí chybou - v globálním scope není dostupná

Lokální proměnná může mít stejný název jako globální, ale je to bad practice, protože to může být poněkud matoucí.

s = "global"

def f():
    s = "local"
    print(s)
    
print(s)
f()
print(s)
global
local
global

Chceme-li z lokálního scope ovlivnit globální proměnnou, je nutné ji označit klíčovým slovem global

s = "global"

def f():
    global s
    s = "local"
    print(s)
    
print(s)
f()
print(s)
global
local
local

Existuje ještě klíčové slovo nonlocal, které má podobný vliv jako global, ale odkazuje se pouze “o scope výš”. To se používá např. u vnořených funkcí

def g():
    s = "g-level"

    def f():
        s = "f-level"
        print(s)

    print(s)
    f()
    print(s)
    
g()
g-level
f-level
g-level
def g():
    s = "g-level"

    def f():
        nonlocal s
        s = "f-level"
        print(s)

    print(s)
    f()
    print(s)
    
g()
g-level
f-level
f-level

Struktura programu, vlastní moduly a balíčky#

Tvorba vlastních modulů a balíčků je v pythonu jednoduchá. Začněme modulem.

Modul#

Co o modulu říká dokumentace

A module is a file containing Python definitions and statements. The file name is the module name with the suffix .py appended. Within a module, the module’s name (as a string) is available as the value of the global variable __name__.

Vytvořme si tedy jednoduchý modul my_module obsahující funkci is_prime

# my_module.py

def is_prime(n: int) -> bool:
    """
    Checks whether n is a prime
    
    Args:
        n: number to be checked
    Returns:
        True if n is a prime
    """
    if n < 2:
        return False
    for i in range(2, n):
        if (n % i) == 0:
            return False
    return True

Z existující modul můžeme importovat nebo importovat z něho

import my_module

my_module.is_prime(4)

případně

from my_module import is_prime

is_prime(4)

V každém modulu je definována proměnná __name__, ve které je buď název modulu, je-li importován, nebo string __main__. Toho se využívá pro oddělení definic a kódu určeného k vykonání při spuštení modulu. Příklad:

# module.py

def say_my_name():
    print(__name__)
    
if __name__ == "__main__":
    say_my_name()

Dostaneme různé výsledky, když modul přímo spustíme:

$ python3 module.py
__main__

a když funkci say_my_name importujeme a spustíme jinde

# main.py

from module import say_my_name

say_my_name()

tedy

$ python3 main.py
module
Proč oddělovat main()?#
  • Python narozdíl od řady jazyků nevyžaduje definovanou main() funkci, je to hezký zvyk, který kód činí poněkud přehlednějším

  • Občas se hodí, aby modul bylo možné jak spustit, tak importovat. Při importu module se vykoná veškerý kód v globálním kontextu, což může mít nepříjmené důsledky.

Příklad:

Balíček / package#

Balíček je složka, která obsahuje více modulů a soubor __init__.py. Může obsahovat i podsložky (subpackages), ale každá z nich musí obsahovat __init__.py

Soubor __init__.py má dvojí význam:

  • říká Pythonu, že složka s module je balíček. Bez __init__.py není možné z balíčku nic importovat.

  • může obsahovat nějaké “package-level” definice, typicky upřesňuje importy.

Napsal jsem balíček ale python hlásí No module named '...'#

Pythoní interpret při importu balíčků prohledává pár typických míst, kde by se balíčky mohly nacházet. Aby našel náš balíček, můžeme…

…říct pythonu, kam se má podívat#
  • tedy do systémové proměnné PYTHONPATH přidat cestu k našemu balíčku

    # linux/mac
    export PYTHONPATH=/path/to/our/package:${PYTHONPATH}
    
  • ve Windows je to komplikovanější -> raději druhá cesta

…balíček nainstalovat#

Pokud balíček nainstalujeme pomocí pip, objeví se na místech, kde ho python běžně hledá (systémová instalace, či ještě lépe - naše virtuální prostředí) a tedy půjde importovat.

Pro instalaci je nutné zavést skript setup.py - ukázka v přiloženém balíčku.

Balíček lze instalovat v “develop” módu - při změnách není nutné balíček přeinstalovat, aby se projevily. Instalace probíhá příkazem

pip3 install -e /path/to/folder/with/setup.py/
  • Přepínač -e zajišťuje právě develop mód.

  • setup.py by se měl nacházet v složce obsahující balíček - viz příklad z hodiny.

Podrobnější návod: Arjan codes

Úkázkový balík mipy_package#

Obsahuje pár funkcí ze semestru, opatřených jednoduchými docstringy a rozdělených do několik modulů. Součástí je připravený jednoduchý setup.py pro instalaci a pár ukázkových testů s využitím balíku pytest (nutné doinstalovat, ještě se o nich budeme bavit).

Dokumentace#

Docstring#

Docstring conventions jsou popsané ve PEP 257.

a = """dlouhe
    stringy

"""
print(a)
dlouhe
    stringy
def polynom(x, *coefs):
    """This function evaluates a polynomial with coeficients coef."""
    val = 0.0
    for i, coef in enumerate(coefs):
        val += coef * x**i
    return val
def f(a, b):
    """
    reST style docstring

    :param a: this is the first number
    :param b: this is the second number
    :returns: the sum of the two numbers
    """
    return a+b

def g(a, b):
    """
    Google style docstring

    Args:
        a: this is the first number
        b: this is the second number
    
    Returns:
        the sum of the two numbers
    """
    return a+b

help(g)
Help on function g in module __main__:

g(a, b)
    Google style docstring
    
    Args:
        a: this is the first number
        b: this is the second number
    
    Returns:
        the sum of the two numbers

Dokumentace#

Ke generování dokumentace používáme standardní balík sphinx, který posbírá všechny docstringy a postaví výsledný dokument ve zvoleném formátu. Dokumentace ke balíku sphinx je celkem podrobná a popisuje mnoho různých možností. Extrahoval jsem jednodušší ukázkový příklad.

Je nutné, aby byl balíček k nalezení (viz výše), jinak postup nebude fungovat. Generování probíhá v několika krocích:

  • pip3 install sphinx

  • cd path/to/package/

  • sphinx-apidoc -F -o docs --ext-autodoc <package>

    • -F vygeneruje vsechno

    • -o vystupni cesta k dokumentaci

    • --ext-autodoc

    • . tento adresar

    • ( -f - force (prepise dokumentaci))

    • pripravi podklady pro generovani dokumentace

  • sphinx-build -b html <src-dir> <output-dir>

    • -b builder. html -> html, pdf->pdf

    • <src-dir> sources k dokumentaci (vygeneroval minuly krok)

    • <output-dir> kam se dokumentace vygeneruje (docs/_build)

Sphinx projde zdrojáky a extrahuje info, které najde v docstringách. Formátů docstringu je několik, nakládání s nimi je popsáno v dokumentaci, např. zde.

Unit testing#

Testování je důležitou součástí vývoj SW, pokud chcete dodávat solidní a funkční produkt. Testování může mít řadu podob, ale zejména u většich projektu se vyplatí přistoupit k tomu poněkud systematičtěji, než pomocí zakomentovaných bloků rozptýlených po zdrojáku.

Spojení unit testing označuje testování atomárních (ve smyslu nedělitelných) úryvků kódu (units). Napsat kvalitní testy a rozumně testovatelný kód chce zkušenost, samotné použití testovacího frameworku lepší kvalitu nezaručuje. V mnoha jazycích je zabudovaná podpora pro unit testing - to kromě základní funkcionality obvykle zahrnuje i pomocné nástroj detekující pokrytí kódu testy atd.

V Pythonu se používají pro testování dva balíky - zabudovaný unittest a externí pytest. Ukážeme si především unittest.

Unittest#

Základními stavebními kameny balíku unittest jsou čtyři koncepty:

  • test fixture (někdy test context) reprezentuje přípravu všeho, co je na daný test třeba (načtení dat, připojení, příprava directory structure)

  • test case testovací jednotka. Typicky určena pro testování jednoho kusu kódu (unit), obsahuje řadu konkrétních testů.

  • test suite agreguje testy do větších celků

  • test runner zajišťuje spouštění testů.

def add(x, y):
    return x + y
import unittest

class TestAdd(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(1, 2), 3)

unittest.main(argv=[''], exit=False)
.

----------------------------------------------------------------------
Ran 1 test in 0.000s

OK
<unittest.main.TestProgram at 0x77c7fa7869b0>
import unittest

class TestAdd(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(1, 2), 3)
        self.assertEqual(add(2, 1), 4)
        print("tady")
        self.assertEqual(add(-1, 2), 1)

unittest.main(argv=[''], exit=False)
F

======================================================================
FAIL: test_add (__main__.TestAdd)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1080365/631272172.py", line 6, in test_add
    self.assertEqual(add(2, 1), 4)
AssertionError: 3 != 4
----------------------------------------------------------------------
Ran 1 test in 0.001s

FAILED (failures=1)
<unittest.main.TestProgram at 0x77c7fa785720>
import unittest
from itertools import product

class TestAdd(unittest.TestCase):
    def test_add(self):
        for a, b in product(range(5), range(6)):
            with self.subTest(a=a, b=b):
                self.assertEqual(add(a, b), a+b)

unittest.main(argv=[''], exit=False)
.

----------------------------------------------------------------------
Ran 1 test in 0.001s

OK
<unittest.main.TestProgram at 0x77c7f860dc30>
import unittest
from itertools import product

def add(x, y):
    if x == 3 and y == 2:
        return x
    if x == 4 and y == 2:
        return x
    return x + y

class TestAdd(unittest.TestCase):
    def test_add(self):
        for a, b in product(range(5), range(6)):
            with self.subTest(a=a, b=b):
                self.assertEqual(add(a, b), a+b)

unittest.main(argv=[''], exit=False)

======================================================================
FAIL: test_add (__main__.TestAdd) (a=3, b=2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1080365/1038771018.py", line 15, in test_add
    self.assertEqual(add(a, b), a+b)
AssertionError: 3 != 5
======================================================================
FAIL: test_add (__main__.TestAdd) (a=4, b=2)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/tmp/ipykernel_1080365/1038771018.py", line 15, in test_add
    self.assertEqual(add(a, b), a+b)
AssertionError: 4 != 6
----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=2)
<unittest.main.TestProgram at 0x77c7f860d1e0>
import unittest

def add(x, y):
    return x + y

class TestAdd(unittest.TestCase):
    def test_add_int(self):
        self.assertEqual(add(1, 2), 3)

    def test_add_str(self):
        self.assertEqual(add("a", "b"), "ab")

    def test_add_float(self):
        self.assertAlmostEqual(add(0.1, 0.2), 0.3)

unittest.main(argv=[''], exit=False)
.
.
.

----------------------------------------------------------------------
Ran 3 tests in 0.001s

OK
<unittest.main.TestProgram at 0x77c7f860fe80>
Mocking and patching#
from unittest.mock import Mock

mock = Mock(return_value=4)
result = mock(1, 2, 3)
mock.assert_called_with(1, 2)
print(result)
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
/tmp/ipykernel_1080365/1251938532.py in <module>
      3 mock = Mock(return_value=4)
      4 result = mock(1, 2, 3)
----> 5 mock.assert_called_with(1, 2)
      6 print(result)

/usr/lib/python3.10/unittest/mock.py in assert_called_with(self, *args, **kwargs)
    927         if actual != expected:
    928             cause = expected if isinstance(expected, Exception) else None
--> 929             raise AssertionError(_error_message()) from cause
    930 
    931 

AssertionError: expected call not found.
Expected: mock(1, 2)
Actual: mock(1, 2, 3)
import math
from unittest.mock import patch

@patch("math.sqrt")
def f(x, mock):
    mock.side_effect = lambda y: y
    print(math.sqrt(x))

f(100)
math.sqrt(4)
import math
from unittest.mock import patch

with patch("math.sqrt") as mock:
    mock.side_effect = lambda y: y
    print(math.sqrt(5))
import math
from unittest.mock import patch

with patch("math.sqrt", side_effect=lambda x: x):
    print(math.sqrt(5))

math.sqrt(5)

Interfaces#

Interface#

CLI - command line interface#

Balíček argparse#

viz příklad

Balíček cmd#

Balíček cmd v Pythonu je určen pro tvorbu jednoduchých interaktivních příkazových řádkových aplikací (CLI).

Klíčové vlastnosti#
  • Snadná Tvorba CLI: Umožňuje rychlé vytvoření interaktivních příkazových řádkových aplikací.

  • Podpora Příkazů: Uživatelé mohou definovat vlastní příkazy a jejich chování.

  • Interaktivní Shell: Poskytuje interaktivní shell, kde uživatelé mohou zadávat příkazy.

  • Pomoc a Dokumentace: Automaticky generuje pomoc pro uživatele založenou na dostupných příkazech a jejich dokumentaci.

  • Rozšiřitelnost: Lze snadno rozšířit a přizpůsobit pro specifické požadavky aplikace.

Využití#
  • Vývoj CLI Nástrojů: Ideální pro vytváření přizpůsobených nástrojů příkazového řádku pro různé účely.

  • Testování a Prototypování: Umožňuje rychlé prototypování a testování CLI interakcí.

Omezení#
  • Jednoduchost: Přestože je to výhoda pro rychlý vývoj, může být omezení pro složitější aplikace.

  • Grafické Rozhraní: Neumožňuje vytvářet grafické uživatelské rozhraní, je zaměřeno pouze na příkazový řádek.

Balíček curses#

curses je programovací knihovna pro vytváření textově orientovaných uživatelských rozhraní v terminálových aplikacích. Je to obzvláště populární v prostředí Unixu a Linuxu.

Klíčové vlastnosti#
  • Textové GUI: Umožňuje vytvářet textové uživatelské rozhraní podobné GUI, ale v terminálu.

  • Ovládání rozložení: Nabízí kontrolu nad umístěním textu, okny a dalšími elementy v terminálu.

  • Rozšířené vstupní možnosti: Podporuje různé klávesové zkratky a kombinace kláves.

  • Barevná schémata: Umožňuje použití různých barevných schémat pro zlepšení čitelnosti a estetiky.

  • Optimalizace výkonu: Efektivně spravuje výstup na obrazovku, což zvyšuje výkon aplikací v terminálu.

Využití#
  • Terminálové aplikace: Ideální pro vytváření aplikací, které běží přímo v textovém terminálu.

  • Interaktivní nástroje: Používá se pro vytváření interaktivních nástrojů, které vyžadují uživatelský vstup a zobrazují složitější výstup.

Omezení#
  • Specifické pro Terminál: Nejlepší využití nachází v prostředí terminálu, není vhodná pro grafické aplikace.

  • Kompatibilita: Primárně určena pro Unix a Linux, i když existují verze pro jiné systémy.

  • Složitost: Může být složitější na naučení a použití ve srovnání s grafickými GUI toolkitů.

GUI - graphical user interface#

tkinter#
  • interface pro Tk (widget toolkit napsaný v jazyce Tcl)

  • výhody:

    • simple

    • cross-platform

    • quick

  • nevýhody

    • ošklivý

  • standardní výbava v Pythonu. Celkem jednoduché dát dohromady prosté GUI

  • součástí spousta standardních widgets - buttons, frames, text boxes, menu etc.

  • napíšeme si GUI pro todolist

ttk bootstrap#

https://ttkbootstrap.readthedocs.io/en/latest/#sample-themes israel-dryer/ttkbootstrap

Qt#
Klady#
  • Kompatibilita napříč platformami: Funguje na Windows, macOS, Linuxu a různých mobilních platformách.

  • Bohatá sada funkcí: Nabízí pokročilé komponenty GUI, 2D/3D grafiku, síťovou podporu a multimédia.

  • Vysoký výkon: Napsáno v C++, což zajišťuje rychlé provedení vhodné pro aplikace s vysokým výkonem.

  • Silná komunita: Velká komunita a obsáhlá dokumentace.

Zápory#
  • Složitost: Kvůli rozsáhlé sadě funkcí může být příliš složité pro začátečníky.

  • Velikost: Větší stopa než některé jiné nástroje pro GUI, což nemusí být ideální pro velmi lehké aplikace.

Omezení#
  • Založeno na C++: I když nabízí více kontroly a výkonu, C++ je složitější než Python, což může být pro některé vývojáře překážkou.

PyQt#
Klady#
  • Integrace s Pythonem: Umožňuje vývojářům v Pythonu přístup k funkcím Qt, kombinuje jednoduchost Pythonu s možnostmi Qt.

  • Bohaté knihovny: Dědí rozsáhlé knihovny a funkce Qt pro pokročilý vývoj GUI.

  • Silná dokumentace: Dobře zdokumentováno, což usnadňuje učení a implementaci.

Zápory#
  • Licencování: Duální model licencování (GPL a komerční) může být omezením pro některé projekty.

  • Learning curve: Přestože je v Pythonu, rozsah knihoven Qt znamená strmější křivku učení ve srovnání s jednoduššími nástroji pro GUI.

Omezení#
  • Přídavná zátěž výkonu: Další vrstva Pythonu může přinést nějaké ztráty výkonu ve srovnání s použitím Qt v C++.

  • Menší kontrola: Ačkoli Python zjednodušuje vývoj, nabízí o něco méně kontroly nad nízkoúrovňovými aspekty než C++.

Honorable mentions#

Sample aplikace pro interface#

pickle#

pickle is a module in Python used for serializing and deserializing objects. Serialization, also known as pickling, involves converting a Python object into a byte stream, and deserialization, or unpickling, converts the byte stream back into an object.

import pickle

# Example object
data = {'key': 'value', 'number': 42}

# Pickling the object
with open('data.pickle', 'wb') as file:
    pickle.dump(data, file)
import pickle

# Unpickling the object
with open('data.pickle', 'rb') as file:
    loaded_data = pickle.load(file)

print(loaded_data)
{'key': 'value', 'number': 42}

Todolist#

import pickle


class Todolist:
    def __init__(self):
        self._todos = []
        self._finished = []

    def create_todo(self, what):
        self._todos.append(what)

    def last_todo(self):
        return self._todos[-1]

    def remove_todo(self, index):
        return self._todos.pop(index - 1)

    def __len__(self):
        return len(self._todos)

    def finish_todo(self, index):
        todo = self._todos.pop(index-1)
        self._finished.append(todo)

    def get_todos(self):
        return [t for t in enumerate(self._todos, start=1)]

    def get_finished(self):
        return [f for f in self._finished]

    def save_to_file(self, filename):
        with open(filename, "wb") as file:
            pickle.dump(self, file)

    @staticmethod
    def load_from_file(filename):
        with open(filename, "rb") as file:
            return pickle.load(file)

CMD#

import cmd

from todolist import Todolist


class Todo(cmd.Cmd):
    intro = "This is a CLI for the todolist"
    prompt = "$ "

    def __init__(self):
        super().__init__()
        self.todolist = Todolist.load_from_file("todolist.pkl")

    def do_finish(self, arg):
        ids = tuple(map(int, arg.split()))
        for _id in ids:
            self.todolist.finish_todo(_id)

    def do_remove(self, arg):
        ids = tuple(map(int, arg.split()))
        for _id in ids:
            self.todolist.remove_todo(_id)

    def do_add(self, arg):
        self.todolist.create_todo(arg)

    def do_list(self, arg):
        print(arg)
        todos = self.todolist.get_todos()
        finished = self.todolist.get_finished()

        if len(todos) > 0:
            print("\nÚKOLY\n=====")
            for i, t in todos:
                print(f"{i}: {t}")
        else:
            print("Žádné úkoly ke splnění")

        if len(finished) > 0:
            print("\nHOTOVO\n======")
            for todo in finished:
                print(f"{todo}")
        print()

    def do_save(self, arg):
        self.todolist.save_to_file("todolist.pkl")

    def do_end(self, arg):
        print("Bye")
        return True


if __name__ == "__main__":
    Todo().cmdloop()

TKinter#

import tkinter as tk

from todolist import Todolist


class SimpleApp:
    def __init__(self, root):
        self.root = root
        self.root.title("TODO list")

        self.todolist = Todolist()
        self.label_todo = tk.Label(root, text="TODO")
        self.label_todo.grid(row=0, column=0, padx=10, pady=10)

        self.label_finished = tk.Label(root, text="FINISHED")
        self.label_finished.grid(row=0, column=2, padx=10, pady=10)

        # Create a Listbox in the first column
        self.listbox = tk.Listbox(root)
        self.listbox.grid(row=1, column=0, rowspan=4, padx=10, pady=10)

        self.finished = tk.Listbox(root)
        self.finished.grid(row=1, column=2, rowspan=4, padx=10, pady=10)

        # Create a Text Entry Field in the second column
        self.entry = tk.Entry(root)
        self.entry.grid(row=1, column=1, padx=10, pady=10)

        # Create an Add Button in the second column
        self.add_button = tk.Button(root, text="Add", command=self.add_item)
        self.add_button.grid(row=2, column=1, padx=10, pady=10)
        self.finish_button = tk.Button(root, text="Finish", command=self.finish_item)
        self.finish_button.grid(row=3, column=1, padx=10, pady=10)

        # Create a Delete Button in the second column
        self.delete_button = tk.Button(root, text="Delete", command=self.delete_item)
        self.delete_button.grid(row=4, column=1, padx=10, pady=10)

    def add_item(self):
        todo = self.entry.get()
        self.entry.delete(0, tk.END)
        self.todolist.create_todo(todo)
        self.update_listboxes()

    def update_listboxes(self):
        self.listbox.delete(0, tk.END)
        for _, t in self.todolist.get_todos():
            self.listbox.insert(tk.END, t)

        self.finished.delete(0, tk.END)
        for t in self.todolist.get_finished():
            self.finished.insert(tk.END, t)

    def finish_item(self):
        try:
            selected_index = self.listbox.curselection()[0]
            self.todolist.finish_todo(selected_index + 1)
            self.update_listboxes()
        except IndexError:
            pass

    def delete_item(self):
        try:
            selected_index = self.listbox.curselection()[0]
            self.todolist.remove_todo(selected_index + 1)
            self.update_listboxes()
        except IndexError:
            pass


def main():
    root = tk.Tk()
    app = SimpleApp(root)
    root.mainloop()


if __name__ == "__main__":
    main()

Plotly#

Plotly#

  • plotly.js je JavaScriptovy engine pro renderovani interaktivnich grafu

  • Plotly v pythonu je pouze interface pro plotly.js -> vetsina vstupu bude prostrednictvim JSONu - JavaScript Object Notation

# zaklad: plotly express
import numpy as np
import plotly.express as px
import plotly.io as pio

pio.templates.default = "simple_white"

x = np.linspace(0, 2*np.pi, 1000)
y = np.sin(x)


fig = px.line(x = x, y = y, labels = {'x' : 'xlabel', 'y' : 'ylabel'}, title = "Prvni plotly graf")
fig.show()
import pandas as pd

df = pd.read_csv("data/nakazeni-vyleceni-umrti-testy.csv")

df.set_index(['datum'], drop = True, inplace = True)
df.index = pd.to_datetime(df.index)
new_names = ["inf", "cur", "dead", "tests", "ag", "d_inf", "d_cur", "d_dead", "d_tests", "d_ag"]
df.columns = new_names
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
/tmp/ipykernel_1080430/3946426501.py in <module>
      1 import pandas as pd
      2 
----> 3 df = pd.read_csv("data/nakazeni-vyleceni-umrti-testy.csv")
      4 
      5 df.set_index(['datum'], drop = True, inplace = True)

~/.local/lib/python3.10/site-packages/pandas/io/parsers/readers.py in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, date_format, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options, dtype_backend)
   1024     kwds.update(kwds_defaults)
   1025 
-> 1026     return _read(filepath_or_buffer, kwds)
   1027 
   1028 

~/.local/lib/python3.10/site-packages/pandas/io/parsers/readers.py in _read(filepath_or_buffer, kwds)
    618 
    619     # Create the parser.
--> 620     parser = TextFileReader(filepath_or_buffer, **kwds)
    621 
    622     if chunksize or iterator:

~/.local/lib/python3.10/site-packages/pandas/io/parsers/readers.py in __init__(self, f, engine, **kwds)
   1618 
   1619         self.handles: IOHandles | None = None
-> 1620         self._engine = self._make_engine(f, self.engine)
   1621 
   1622     def close(self) -> None:

~/.local/lib/python3.10/site-packages/pandas/io/parsers/readers.py in _make_engine(self, f, engine)
   1878                 if "b" not in mode:
   1879                     mode += "b"
-> 1880             self.handles = get_handle(
   1881                 f,
   1882                 mode,

~/.local/lib/python3.10/site-packages/pandas/io/common.py in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    871         if ioargs.encoding and "b" not in ioargs.mode:
    872             # Encoding
--> 873             handle = open(
    874                 handle,
    875                 ioargs.mode,

FileNotFoundError: [Errno 2] No such file or directory: 'data/nakazeni-vyleceni-umrti-testy.csv'
import plotly.express as px
df2 = df.resample("5D").agg({"d_inf" : "mean", "d_dead" : "mean"})
fig = px.line(df2,
              y = ['d_inf', 'd_dead'],
              markers = True,
              title = "Vyvoj epidemie COVID19",
              labels = {"datum" : "Datum", "value" : "Pocet"},
              color_discrete_map = {"d_inf" : "green", "d_dead" : "red"},
              template = "simple_white"
             )
fig.show()
import plotly.graph_objects as go


fig = go.Figure()
fig.add_trace(go.Scatter(x = df.index, y = df["d_dead"], name = "Denní přírůstek úmrtí"))
fig.add_trace(go.Scatter(x = df2.index, y = df2["d_dead"], name = "5-denní klouzavý průměr"))
fig.update_layout(template = "plotly_dark", title = "Vývoj epidemie COVID19")
fig.show()
df3 = pd.DataFrame(df[["cur", "dead"]].iloc[-1])
df3.columns = ["curvsdead"]
df3["names"] = ["Vyléčení", "Zemřelí"]

import plotly.express as px
fig = px.pie(df3, values = "curvsdead", names = "names", labels = {"curvsdead" : "Počet"}, template = "plotly_dark")
fig.show()
from plotly.subplots import make_subplots
import plotly.graph_objects as go

fig = make_subplots(rows = 1, cols = 2, specs = [[{"type" : "xy"}, {"type" : "pie"}]])
fig.update_layout(template = "plotly_dark", title = "Vývoj epidemie COVID19")

dead = go.Scatter(x = df.index, y = df["d_dead"], name = "Denní přírůstek úmrtí", line={"color" : "red"})
dead_avg = go.Scatter(x = df2.index, y = df2["d_dead"], name = "5-denní klouzavý průměr", line = {"color" : "lightgreen"})
kolac = go.Pie(values = df3["curvsdead"], labels = df3["names"])

fig.add_trace(dead, row = 1, col = 1)
fig.add_trace(dead_avg, row = 1, col = 1)
fig.add_trace(kolac, row = 1, col = 2)

fig.show()
df4 = df.resample("2M").agg({"d_inf" : "sum", "d_cur" : "sum"})

fig = go.Figure()

d_cur = go.Bar(name = "vyleceni", x = df4.index, y = df4["d_cur"])
d_inf = go.Bar(name = "infikovani", x = df4.index, y = df4["d_inf"])
fig.add_trace(d_cur)
fig.add_trace(d_inf)
fig.show()
df4 = df.resample("2M").agg({"d_dead" : "sum", "d_cur" : "sum"})

fig = go.Figure()

d_cur = go.Bar(name = "vyleceni", x = df4.index, y = df4["d_cur"])
d_dead = go.Bar(name = "zemreli", x = df4.index, y = df4["d_dead"])
fig.add_trace(d_cur)
fig.add_trace(d_dead)
fig.update_layout(barmode='stack', title = "Dvoumesicni ubytek nakazenych", template = "plotly_dark")
fig.show()

detailnější#

import plotly.graph_objects as go


fig = go.Figure()
dead = go.Scatter(x = df.index, y = df["d_dead"], name = "Denní přírůstek úmrtí")
dead_avg = go.Scatter(x = df2.index, y = df2["d_dead"], name = "5-denní klouzavý průměr")

fig.add_trace(dead)
fig.add_trace(dead_avg)
fig.update_layout(
    template="simple_white",
    title="Vývoj epidemie COVID19",
    xaxis_title="čas",
    yaxis_title="počet úmrtí (ks)",
    legend={
        "yanchor": "top",
        "y": 0.20
    }
)

fig.update_traces(
    selector=dict(name="Denní přírůstek úmrtí"),
    line=dict(color="yellow")
)

fig.data[1].line.width=5

fig.show()

Dash#

https://dash.plotly.com/introduction

  • hello world (jupyter vs nejupyter)

  • callback

  • dash + plotly

from jupyter_dash import JupyterDash
from dash import dcc # dash core components
from dash import html
from dash.dependencies import Input, Output

app = JupyterDash(__name__)

pozdrav = {
    "de" : "Hallo",
    "en" : "Oi, oi",
    "cz" : "Nazdar",
    "jp" : "こんにちは"
}

app.layout = html.Div([
    html.H1("Hallo", id = "header"),
    dcc.RadioItems(
        id = "jazyk",
        options = [
            {"value" : "de", "label" : "Deutsch"},
            {"value" : "en", "label" : "English"},
            {"value" : "cz", "label" : "Česky"},
            {"value" : "jp", "label" : "日本語"}
        ],
        value = "jp"),
    html.Hr()
])

@app.callback(
    Output("header", "children"),
    Input("jazyk", "value"))
def update_hello(val):
    return pozdrav[val]

app.run_server(mode="inline")
---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
/tmp/ipykernel_1080399/3609927044.py in <module>
----> 1 from jupyter_dash import JupyterDash
      2 from dash import dcc # dash core components
      3 from dash import html
      4 from dash.dependencies import Input, Output
      5 

ModuleNotFoundError: No module named 'jupyter_dash'
from jupyter_dash import JupyterDash
from dash import dcc # dash core components
from dash import html
from dash.dependencies import Input, Output

import pandas as pd
import plotly.graph_objects as go

df = pd.read_csv("./data/nakazeni-vyleceni-umrti-testy.csv")

df.set_index(['datum'], drop = True, inplace = True)
df.index = pd.to_datetime(df.index)
new_names = ["inf", "cur", "dead", "tests", "ag", "d_inf", "d_cur", "d_dead", "d_tests", "d_ag"]

df.columns = new_names

df = df[["inf", "dead", "d_inf", "d_dead"]]
pretty_names = {
    "inf" : "Nakažení celkem",
    "dead": "Mrtví celkem",
    "d_inf" : "Přírůstek nakažených",
    "d_dead" : "Přírůstek mrtvých"
}

aggr = {"inf" : "sum", "dead" : "sum", "d_inf" : "mean", "d_dead" : "mean"}

app = JupyterDash(__name__)
app.layout = html.Div([
    html.H1("Covid"),
    html.Label([
        "theme",
        dcc.Dropdown(
            id='theme-dropdown', clearable=False,
            value='plotly_white',
            options=[{'label': c, 'value': c} for c in ["plotly_white", "plotly_dark"]],
            )]),
    html.Label([
        "Data",
        dcc.Checklist(
            id='data-checklist',
            value=['d_dead'],
            options=[{'label': pretty_names[d], 'value': d} for d in ["d_inf", "d_dead"]]
            )
    ]),
    html.Label([
        "Klouzavý průměr ",
        dcc.Input(
            id="days", type="number", value = 1,
            min=1, max=10, step=1,
        )
    ]),
    dcc.Graph(id="graph"),
    html.Hr(),
    html.Label([
        "Data",
        dcc.Checklist(
            id='data-cummulative',
            value=['dead'],
            options=[{'label': pretty_names[d], 'value': d} for d in ["inf", "dead"]],
            )
    ]),
    dcc.Checklist(
        id='logscale',
        value = [],
        options=[{'label': "Logaritmická škála", 'value': "logscale_y"}],
    ),
    dcc.Graph(id="graph2"),
])

@app.callback(
    Output("graph", "figure"),
    Input("theme-dropdown", "value"),
    Input("data-checklist", "value"),
    Input("days", "value"))
def update_figure1(theme, data, days):
    if days is None:
        days = 1
    fig = go.Figure()
    for dt in data:
        fig.add_trace(go.Scatter(x = df.index, y = df[dt], name = pretty_names[dt]))

    if days > 1:
        resampled = df.resample("%dD" % days).agg(aggr)
        for dt in data:
            fig.add_trace(go.Scatter(x = resampled.index, y = resampled[dt],
                                     name = pretty_names[dt] + f" {days}D avg"))
        
    fig.update_layout(template = theme, title = "COVID19 - Přírůstky")
    return fig


@app.callback(
    Output("graph2", "figure"),
    Input("theme-dropdown", "value"),
    Input("data-cummulative", "value"),
    Input("logscale", "value"))
def update_figure2(theme, data, logscale):
    if len(logscale) == 0:
        logscale = False
    fig = go.Figure()

    for dt in data:
        fig.add_trace(go.Scatter(x = df.index, y = df[dt], name = pretty_names[dt]))
    
    if logscale:
        fig.update_yaxes(type="log")    
    fig.update_layout(template = theme, title = "COVID19 - Kumulativní počty")
    return fig

app.run_server(mode="inline")
import plotly.express as px
from jupyter_dash import JupyterDash
from dash import dcc
from dash import html

from dash.dependencies import Input, Output
# Load Data
df = px.data.tips()
# Build App
app = JupyterDash(__name__)


app.layout = html.Div([
    html.H1("JupyterDash Demo"),
    dcc.Graph(id='graph'),
    html.Label([
        "colorscale",
        dcc.Dropdown(
            id='colorscale-dropdown', clearable=False,
            value='plasma', options=[
                {'label': c, 'value': c}
                for c in px.colors.named_colorscales()
            ])
    ]),
])

# Define callback to update graph
@app.callback(
    Output('graph', 'figure'),
    [Input("colorscale-dropdown", "value")]
)
def update_figure(colorscale):
    return px.scatter(
        df, x="total_bill", y="tip", color="size", size = "size",
        color_continuous_scale=colorscale,
        render_mode="webgl", title="Tips"
    )
# Run app and display result inline in the notebook
app.run_server(mode='inline')

Dash Bootstrap#

https://getbootstrap.com/

Implementováno v Dash

http://dash-bootstrap-components.opensource.faculty.ai/

import dash
import dash_bootstrap_components as dbc
from dash import html
from dash import dcc
from jupyter_dash import JupyterDash
import pandas as pd
from dash.dependencies import Input, Output
import plotly.graph_objects as go

df = pd.read_csv("data/nakazeni-vyleceni-umrti-testy.csv")

df.set_index(['datum'], drop = True, inplace = True)
df.index = pd.to_datetime(df.index)
new_names = ["inf", "cur", "dead", "tests", "ag", "d_inf", "d_cur", "d_dead", "d_tests", "d_ag"]

df.columns = new_names

df = df[["inf", "dead", "d_inf", "d_dead"]]
pretty_names = {
    "inf" : "Nakažení celkem",
    "dead": "Mrtví celkem",
    "d_inf" : "Přírůstek nakažených",
    "d_dead" : "Přírůstek mrtvých"
}

aggr = {"inf" : "sum", "dead" : "sum", "d_inf" : "mean", "d_dead" : "mean"}

app = JupyterDash(external_stylesheets=[dbc.themes.BOOTSTRAP])

carousel = dbc.Carousel(
    items=[
        {"key": "1", "src": "assets/covid_01.jpeg", "img_style": {"object-fit": "contain", "max-height": "250px"}},
        {"key": "2", "src": "assets/covid_02.jpeg", "img_style": {"object-fit": "contain", "max-height": "250px"}},
        {"key": "3", "src": "assets/covid_03.jpeg", "img_style": {"object-fit": "contain", "max-height": "250px"}},
    ],
    controls=False,
    indicators=False,
    interval=2000,
    ride="carousel",
    
)

controls = html.Div([
    dbc.InputGroup([
        dbc.InputGroupText("Klouzavý průměr"),
        dbc.Input(
            id="days",
            type="number",
            value=1,
            min=1,
            max=10,
            step=1
        )
    ]),
    dbc.InputGroup([
        dbc.InputGroupText("Data set"),
        dbc.Checklist(
        options=[{'label': pretty_names[d], 'value': d} for d in ["d_inf", "d_dead"]],
        value=["d_dead"],
        id="data-checklist",
        ),
    ]),
    dbc.InputGroup([
        dbc.InputGroupText("Plotly theme"),
        dbc.Select(
            id='theme-dropdown',
            value='plotly_white',
            options=[{'label': c, 'value': c} for c in ["plotly_white", "plotly_dark"]],

        )
    ])
])

controls2 = html.Div([
     dbc.InputGroup([
        dbc.InputGroupText("Data"),
        dbc.Checklist(
            id='data-cummulative',
            value=['dead'],
            options=[{'label': pretty_names[d], 'value': d} for d in ["inf", "dead"]],
        )
    ]),
    dbc.Checklist(
        id='logscale',
        value = [],
        options=[{'label': "Logaritmická škála", 'value': "logscale_y"}],
    ),
])
accordion = html.Div(
    dbc.Accordion(
        [
            dbc.AccordionItem(
                [
                    controls,
                    dcc.Graph(id="graph")
                ],
                title="Covid",
            ),
            dbc.AccordionItem(
                [
                    controls2,
                    dcc.Graph(id="graph2")
                ],
                title="Taky covid",
            )
        ],
    )
)

app.layout = dbc.Container([carousel, accordion])

@app.callback(
    Output("graph", "figure"),
    Input("theme-dropdown", "value"),
    Input("data-checklist", "value"),
    Input("days", "value"))
def update_figure1(theme, data, days):
    if days is None:
        days = 1
    fig = go.Figure()
    for dt in data:
        fig.add_trace(go.Scatter(x = df.index, y = df[dt], name = pretty_names[dt]))

    if days > 1:
        resampled = df.resample("%dD" % days).agg(aggr)
        for dt in data:
            fig.add_trace(go.Scatter(x = resampled.index, y = resampled[dt],
                                     name = pretty_names[dt] + f" {days}D avg"))
        
    fig.update_layout(template = theme, title = "COVID19 - Přírůstky")
    return fig

@app.callback(
    Output("graph2", "figure"),
    Input("theme-dropdown", "value"),
    Input("data-cummulative", "value"),
    Input("logscale", "value"))
def update_figure2(theme, data, logscale):
    if len(logscale) == 0:
        logscale = False
    fig = go.Figure()

    for dt in data:
        fig.add_trace(go.Scatter(x = df.index, y = df[dt], name = pretty_names[dt]))
    
    if logscale:
        fig.update_yaxes(type="log")    
    fig.update_layout(template = theme, title = "COVID19 - Kumulativní počty")
    return fig


app.run_server()
/data/unicorn/pythonbook/venv/lib/python3.10/site-packages/dash/dash.py:538: UserWarning:

JupyterDash is deprecated, use Dash instead.
See https://dash.plotly.com/dash-in-jupyter for more details.
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/data/unicorn/pythonbook/introduction/dash_02.ipynb Cell 6 line 1
    <a href='vscode-notebook-cell:/data/unicorn/pythonbook/introduction/dash_02.ipynb#W5sZmlsZQ%3D%3D?line=147'>148</a>     fig.update_layout(template = theme, title = "COVID19 - Kumulativní počty")
    <a href='vscode-notebook-cell:/data/unicorn/pythonbook/introduction/dash_02.ipynb#W5sZmlsZQ%3D%3D?line=148'>149</a>     return fig
--> <a href='vscode-notebook-cell:/data/unicorn/pythonbook/introduction/dash_02.ipynb#W5sZmlsZQ%3D%3D?line=151'>152</a> app.run_server()

File /data/unicorn/pythonbook/venv/lib/python3.10/site-packages/jupyter_dash/jupyter_app.py:222, in JupyterDash.run_server(self, mode, width, height, inline_exceptions, **kwargs)
    220 old_server = self._server_threads.get((host, port))
    221 if old_server:
--> 222     old_server.kill()
    223     old_server.join()
    224     del self._server_threads[(host, port)]

File /data/unicorn/pythonbook/venv/lib/python3.10/site-packages/jupyter_dash/_stoppable_thread.py:16, in StoppableThread.kill(self)
     13 def kill(self):
     14     thread_id = self.get_id()
     15     res = ctypes.pythonapi.PyThreadState_SetAsyncExc(
---> 16         ctypes.c_long(thread_id), ctypes.py_object(SystemExit)
     17     )
     18     if res == 0:
     19         raise ValueError(f"Invalid thread id: {thread_id}")

TypeError: 'NoneType' object cannot be interpreted as an integer

Úlohy#

Fahrenheitova stupnice#

Německý fyzik Gabriel Fahrenheit navrhl teplotní stupnici se dvěma skvěle zvolenými referenčními body. Teplota 0 °F je nejnižší teplota, jakou se podařilo Fahrenheitovi naměřit (v roce 1724) ve směsi chloridu amonného, vody a ledu. Druhým referenčním bodem pak je normální teplota lidského těla, fixovaná na 98 °F. Nyní se jako referenční body používají bod mrazu vody (32 °F) a bod varu vody (212 °F), čemuž odpovídají převodní vztahy

\[ T_C = (T_F - 32) \frac{5}{9} \]
\[ T_F = \frac{9}{5} T_C + 32 \]

Napište funkce get_fahrenheit(tc) a get_celsius(tf), které převedou teplotu do druhé stupnice.

Řešení#

Hide code cell source
     
def get_fahrenheit(tc):
    return tc * 9. / 5. + 32.

def get_celsius(tf):
    return (tf - 32) * 5. / 9.

def get_celsius2(tf):
    return tf * 5./9. - 32. * 5 / 9.
tf = 32.6
tc1 = get_celsius(tf)
tc2 = get_celsius2(tf)

print(tc1, tc2, tc1 == tc2)

print(abs(tc1 - tc2) <= 1e-14)
0.33333333333333415 0.33333333333333215 False
True

Tisk účtenky#

Napište funkci

def get_receipt(nakup: dict, width=50) -> str:
    ...

která ze slovníku s nákupem vytvoří řetězec s width znaků širokou účtenkou následujícího formátu:

nakup = {
    rohliky: 40,
    pivo: 1200
}

receipt = get_receipt(nakup)
print(receipt)
==================== ÚČTENKA =====================
rohliky......................................40.00
pivo.......................................1200.00
--------------------------------------------------
Celkem (Kc)
                                           1240.00

Řešení#

Hide code cell source
def get_receipt(nakup, width=40):
    out_str = " ÚČTENKA ".center(width, "=") + "\n"

    for key, val in nakup.items():
        fill_width = width - len(key)
        out_str += f"{key}{val:.>{fill_width}.2f}\n"

    out_str += (
        f"{'-':->{width}s}\n"
        f"Celkem (Kč)\n"
        f"{sum(nakup.values()): >{width}.2f}"
    )
    return out_str
nakup = {
    "rohliky": 20,
    "salam": 23.9,
    "maslo": 62.5,
    "tuřín": 1348
}

print(get_receipt(nakup, 51))
===================== ÚČTENKA =====================
rohliky.......................................20.00
salam.........................................23.90
maslo.........................................62.50
tuřín.......................................1348.00
---------------------------------------------------
Celkem (Kč)
                                            1454.40

Valid string#

Napište funkci is_valid(instr, forbidden) vracející True/False, která rozhodne, zda je řetězec instr validní, tj. že neobsahuje znaky z řetězce forbidden.

Řešení#

Hide code cell source
def is_valid(instr, forbidden="!@#$%^&*()"):
    for f in forbidden:
        if f in instr:
            return False
    return True
is_valid("abc", forbidden="c")
False

Stabilita algoritmu#

Chceme spočítat následující integrál:

\[ I_n(a) = \int_0^1 \frac{x^n}{x+a}\ dx \]

Integrál leze sice vyřešit analyticky, ale tím se zabývat nebudeme. Řekněme, že si všimnete, že pro tento integrál lze zformulovat rekurentní vzorec v \(n\), tj. \(n\)-tý integrál umíme spočítat z \(n-1\)-tého:

\[ I_n = \frac{1}{n} - a I_{n-1}, \]

přičemž \(I_0(a) = \ln\frac{1+a}{a}\). Vyberme si pro jedoduchost \(a=10\) (potom platí, že \(0 < I_n \leq 1\)).

Spočítejte pomocí rekurentního vzorce pro \(a=10\) hodnotu \(I_{20}\). Správný výsledek je toto:

\[ I_{20} = 0.0043470358180281 \]

Pretty print slovniku#

Napište funkci pretty_print(d), která zadaný slovník vytiskne hezky. Tedy například takto:

>>> d = dict(
    "jmeno" = "Vaclav",
    "prijmeni" = "Alt",
    "adresa" = dict(
        "ulice" = "Na Konci",
        "cislo" = "1",
        "mesto" = "Ricany"
    )
)

>>> pretty_print(d)
jmeno: Vaclav
prijmeni: Alt
adresa: {
  ulice: Na Konci
  cislo: 1
  mesto: Ricany
}

Řešení#

Hide code cell source
def pretty_print(d, indent = 0, c = "\t"):
    white = indent * c
    for key, val in d.items():
        if type(val) == dict:
            print(white + "{}: {{".format(key))
            pretty_print(val, indent + 1, c)
            print(white + "}")
        else:
            print(white + "{}: {}".format(key, val))
d = dict(
    jmeno = "Vaclav",
    prijmeni = "Alt",
    adresa = dict(
        ulice = "Na Konci",
        cislo = "1",
        mesto = "Ricany"
    ),
)

pretty_print(d, 0, "  ")
jmeno: Vaclav
prijmeni: Alt
adresa: {
  ulice: Na Konci
  cislo: 1
  mesto: Ricany
}

Řešení bez rekurze#

Hide code cell source
def pretty_print(d: dict, indent: int = 0, c: str = "\t"):
    print_stack = [(k, v, indent) for k, v in reversed(d.items())]
    while print_stack:
        k, v, indent = print_stack.pop(-1)
        
        if type(v) == dict:
            print_stack.append(("", "}", indent))
            for key, val in reversed(v.items()):
                print_stack.append((key, val, indent+1))
            print_stack.append((k, "{", indent))
            continue

        if k:
            k = f"{k}: "

        print(f"{indent*c}{k}{v}")

pretty_print(d, c="  ")
jmeno: Vaclav
prijmeni: Alt
adresa: {
  ulice: Na Konci
  cislo: 1
  mesto: Ricany
}
pretty_print(d, 0, "  ")
jmeno: Vaclav
prijmeni: Alt
adresa: {
  ulice: Na Konci
  cislo: 1
  mesto: Ricany
}

Casesarova šifra#

Implementujte Caesarovu šifru, která funguje tak, že každý znak nahradí znakem, který je vůči němu v abecedě posunutý o několik míst (budeme tomu říkat shift). Implementace by měla mít podobu dvojice funkcí encode(msg: str, shift: int) -> str a decode(msg: str, shift: int) -> str. Jejich složením byste měli získat původní zprávu. Pro implementaci můžete použít i další funkce, bude-li vám to vyhovovat.

Poznámky:

  1. Zatím předpokládejte, že v kódované zprávě nejsou mezery.

  2. Omezte se na začátek na malé ascii znaky. Najdete je připravené v modulu string v proměnné ascii_lowercase, tj. např. from string import ascii_lowercase

  3. Posuny přes okraje abecedy přesmerujte na druhý okraj, tj. při shift=3 nastane z -> c.

  4. Nezdržujte se ověřováním, zda jsou v původní zprávě pouze ascii znaky a tak podobně, soustřeďte se na jádro problému.

Bonus#

Implementujte Vigenerovu šifru. Ta je velmi podobná Caesarově šifře, jen každý znak kódované zprávy se posouvá různě. Posun konkrétního znaku je dán polohou znaku v klíči. Příklad

# příklad z Wiki, ať to nemusíte opisovat
>>> vigenere_encode("attackatdawn", key="lemon")
lxfopvefrnhr

Znak a se posunul o 11 na l, protože l v klíči je 11 písmenem (počítáno od 1). Znak t se posunul o 4 na x, protože e v klíči je 4. písmenem. Je-li zpráva delší než klíč, používá se klíč pořád dokola.

Nápověda: mrkněte do modulu itertools, jestli tam náhodou není funkce, která by pomohla s opakování klíče.

Řešení 1#

Hide code cell source
from string import ascii_lowercase

inverse_map = {l:i for i, l in enumerate(ascii_lowercase)}


def encode(msg: str, shift: int) -> str:
    """
    Encodes a message msg using Caesar's cipher with given shift
    """
    enc_msg = ""
    for c in msg:
        new_index = inverse_map[c] + shift
        enc_msg += ascii_lowercase[new_index % len(ascii_lowercase)]
    return enc_msg
    
    
def decode(enc_msg: str, shift: int) -> str:
    """
    Decodes a message msg using Caesar's cipher with given shift
    """
    dec_msg = ""
    for c in enc_msg:
        new_index = inverse_map[c] - shift
        dec_msg += ascii_lowercase[new_index % len(ascii_lowercase)]
    return dec_msg
for i in range(len(ascii_lowercase)):
    assert ascii_lowercase == decode(encode(ascii_lowercase, shift=i), shift=i)

Řešení 2 + Bonus#

Hide code cell source
from string import ascii_lowercase

inverse_map = {l:i for i, l in enumerate(ascii_lowercase)}


def caesar_encode_char(c: str, shift: int):
    """
    Encodes a single character using Caesar's cipher with given shift
    """
    new_index = inverse_map[c] + shift
    return ascii_lowercase[new_index % len(ascii_lowercase)]


def caesar_encode(msg: str, shift: int) -> str:
    """
    Encodes a message msg using Caesar's cipher with given shift
    """
    return "".join(caesar_encode_char(c, shift) for c in msg)
    
    
def caesar_decode(msg: str, shift: int) -> str:
    """
    Decodes a message msg using Caesar's cipher with given shift
    """
    return caesar_encode(msg, -shift)
for i in range(len(ascii_lowercase)):
    assert ascii_lowercase == caesar_decode(caesar_encode(ascii_lowercase, shift=i), shift=i)
Hide code cell source
# vigenere
from itertools import cycle

def vigenere_encode(msg: str, key: str) -> str:
    """
    Encodes a message msg with the given key using Vigenere's cipher
    """
    return "".join(
        caesar_encode_char(c, inverse_map[k])
        for c, k in zip(msg, cycle(key))
    )

def vigenere_decode(msg: str, key: str) -> str:
    """
    Decodes a message msg with the given key using Vigenere's cipher
    """
    return "".join(
        caesar_encode_char(c, -inverse_map[k])
        for c, k in zip(msg, cycle(key))
    )
assert "LXFOPVEFRNHR".lower() == vigenere_encode("attackatdawn", "lemon") # priklad z wiki
assert "klobasa" == vigenere_decode(vigenere_encode("klobasa", key="jelito"), key="jelito") # self-consistence

Prvočísla#

Napište funkci is_prime(num), která na vstupu dostane celé číslo a vrátí True/False, podle toho, zda je dané číslo prvočíslem. Dále napište funkci get_primes(numbers), která dostane kolekci čísel a vybere z ní prvočísla a navrátí je jako seznam.

Řešení#

Hide code cell source
def is_prime(n):
    if (n > 1):
        for i in range(2, n):
            if (n % i) == 0:
                return False
        return True
    else:
        return False
    
def get_primes(numbers):
    primes = []
    for i in range(len(numbers)):
        num = numbers[i]
        if (is_prime(num) == True):
            primes.append(num)
        else:
            continue
    return primes
        
numbers = [1, 2, 3, 4, 5, 6]

primes = get_primes(numbers)
print(primes)
[2, 3, 5]

Trochu čistší řešení#

Hide code cell source
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, n):
        if (n % i) == 0:
            return False
    return True

def get_primes(numbers):
    primes = []
    for num in numbers:
        if is_prime(num):
            primes.append(num)
    return primes

Very Pythonic řešení#

Hide code cell source
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, n):
        if (n % i) == 0:
            return False
    return True

def get_primes(numbers):
    return [x for x in numbers if is_prime(x)]

Funkcionální řešení#

Hide code cell source
def is_prime(n):
    if n <= 1:
        return False
    for i in range(2, n):
        if (n % i) == 0:
            return False
    return True

def get_primes(numbers):
    return list(filter(is_prime, numbers))

Monte Carlo - obsah kruhu#

Monte Carlo je třída metod využívajících náhodná čísla k řešení problémů. My zde zkusíme aproximovat hodnotu čísla \(\pi\). Číslo \(\pi\) vyjadřuje poměr mezi obsahem kruhu s poloměrem \(r\) a čtvercem o hraně \(r\). Představmě si čtverec opsaný kruhu (tj. čtverec o hraně \(2r\) s kruhem ve svém středu). Program bude náhodně generovat souřadnice bodů \(x\), \(y\) uvnitř čtvercee a bude ověřovat, zda se nachází i uvnitř kruhu. Číslo \(\pi\) pak půjde odhadnout, zhruba jako poměr bodů v kruhu ku celkovému počtu.

Funkce random.random() generuje náhodná čísla v rozmezí \([0,1)\), takže hrana našeho čtverce bude mít velikost \(1\). Kruh vepsaný tomuto čtverci tak bude mít střed na souřadnicích \([0.5, 0.5]\) a poloměr \(r=0.5\). Souřadnice bodů ležících uvnitř kruhu tedy musí splňovat nerovnici

\[ (x-0.5)^2 + (y-0.5)^2 \leq 0.5^2 \]

Obsah čtverce

\[ S_c = a^2 \]

Obsah maximálního vepsaného kruhu

\[ S_k = \pi \left(\frac{a}{2}\right)^2 \]
\[ \frac{S_k}{S_c} = \frac{\pi}{4} \]

neboli

\[ \pi = 4\frac{S_k}{S_c} \]

Řešení#

Hide code cell source
import random as r


def in_circle(x, y):
    if (x-0.5)**2 + (y-0.5)**2 <= 0.25:
        return True
    return False

inside = 0
N = 10000

for i in range(N):
    x , y = r.random(), r.random()
    if in_circle(x, y):
        inside += 1
        
pi = 4 * inside / N
pi
3.1356

Polynom#

Polynomem \(p(x)\) stupně \(n\) nazýváme výraz tvaru

\[ p(x) = \sum_{i=-1}^n a_i x^i = a_0 + a_1 \cdot x + \ldots + a_n \cdot x^n \]

a čísla \(a_i \in \mathcal{R},\ i=0,\dots n\) se nazývají koeficienty.

Napište funkci, která vyhodnotí polynom zadaný n-ticí koeficientů v bodě x, tj.

# polynom(x, 3, 2, 1) vyhodnoti 3 + 2x + x^2
polynom(0, 3, 2, 1)
>> 3

Je vhodné pro zápis polynomu použít tzv. Hornerovo schéma (ušetří mnoho operací a snáze se implementuje). Pro polynom stupně 2 vypadá takto: $\( a_0 + a_1\cdot x + a_2 \cdot x^2 + a_3 \cdot x^3 = a_0 + x \cdot ( a_1 + x \cdot ( a_2 + a_3 \cdot x) ) \)$

Řešení#

Hide code cell source
def polynom(x, *coefs):
    val = 0.0
    for i, coef in enumerate(coefs):
        val += coef * x**i
    return val
# konstantni funkce: 1 + 0*x; p(2) = 1
print(polynom(2, 1))
# linearni funkce: 0 + 1*x; p(5) = 5
print(polynom(5, 0, 1))
# kvadraticky trojclen: 3 + 2*x + 1*x^2; p(0) = 3
print(polynom(0, 3, 2, 1))
1.0
5.0
3.0

TODO list#

Napište program, který bude spravovat TODO list. Program bude udržovat seznam ukončených a neukončených úkolů, ale nebude je nikam ukládat (po skončení programu oba seznamy zaniknou). Uživatel bude s programem interagovat pomocí následujících jednopísmenných příkazů, které bude zadávat do příkazové řádky:

  • c: vytvořit úkol

  • r: odstranit úkol

  • f: ukončit úkol

  • l: vypsat seznam úkolů

  • h: zobrazit nápovědu

  • q: ukončit program

Příkazy c, r a f budou vyžadovat ještě argument - obsah úkolu, případně index úkolu, který se má odstranit nebo ukončit. Argument bude uživatel zadávat až po volbě příkazu (viz ukázka použití na konci). Nápověda:

  • pro čtení uživatelského vstupu použijte funkci input(prompt)

  • program můžete ukončit pomocí sys.exit(message) (nutno importovat sys). Není to hezké, ale je to snadné.

> c
Zadejte nový úkol: vyprat
> c
Zadejte nový úkol: vynést odpad
> l

ÚKOLY
=====
1. vyprat
2. vynést odpad

> f
Zadejte číslo úkolu k ukončení: 1

> l 

ÚKOLY
=====
1. vynést odpad

HOTOVÉ
======
1. vyprat

> r
Zadejte číslo úkolu k odstranění: 1

> l

Žádné úkoly ke splnění

HOTOVÉ
======
1. vyprat

>

Úlohy pro 4. týden#

Filtrování#

employees = [
    {"name": "Petr", "age": 23, "job": "správce budovy"},
    {"name": "Jarda", "age": 48, "job": "dozor nad gorilami"},
    {"name": "Karolína", "age": 43, "job": "dozor nad Jardou"},
    {"name": "Cecil", "age": 27, "job": "gorila"},
]
def get_older_than(employees: list[dict], age: int) -> list[str]:
    return [c["name"] for c in employees if c["age"] > age]

get_older_than(employees, 30)
['Jarda', 'Karolína']
def get_older_than(employees: list[dict], age: int) -> list[str]:
    return list(map(lambda x: x["name"], filter(lambda x: x["age"] > age, employees)))

get_older_than(employees, 30)
['Jarda', 'Karolína']

Smíšené typy#

def process_items(items: list[int | str]) -> tuple[list[int], list[str]]:
    squares: list[int] = []
    strings: list[str] = []
    for item in items:
        if type(item) == int:
            squares.append(item**2)
        elif type(item) == str:
            strings.append(item.upper())
    return squares, strings

process_items([2, 3, "jogurt", 1, "zeli"])
([4, 9, 1], ['JOGURT', 'ZELI'])

Low-level commad#

from typing import Optional
commands = {}

def show_help():
    for code, (_, helpstr, args) in commands.items():
        print(f"{code}: {helpstr}, {args}")


def read_command(instr: str) -> tuple[str, list[str]]:
    tokens = instr.split()

    code = tokens[0]
    args = tokens[1:] if len(tokens) > 1 else []
    return code, args


def parse_args(code: str, args: list[str]) -> Optional[list]:
    _, _, argtypes = commands[code]
    if argtypes is None:
        return

    if len(args) !=  len(argtypes):
        raise ValueError("Incorrect number of parameters")
    
    return [conv(a) for conv, a in zip(argtypes, args)]

    
def run_command(code: str, args):
    fun, _, _ = commands[code]
    if args is None:
        fun()
    else:
        fun(*args)


def main_loop(app_name="Some app"):
    print(f"Welcome to {app_name}! Type 'help' for help.")
    while True:
        instr = input("> ")
        code, args = read_command(instr)

        if code not in commands:
            print(f"unknown command {code}, try again")
            continue

        try:
            args = parse_args(code, args)
        except ValueError as e:
            print("could not parse arguments. Read help and try again")
            print(e)
            continue

        run_command(code, args)
            


commands = {
    "help": (show_help, "zobrazi napovedu", None),
}

Todo list with above command#

Nákupní košík#

Implementujte třídy ShoppingItem a ShoppingCart. Třída ShoppingItem reprezentuje libovolnou nákupní položku. Každá její instance bude mít atributy name a price. Nákupní košík pak reprezentuje ShoppingCart. Do nákupního košíku lze přidávat položky pomocí metody .add_item(item: ShoppingItem, quantity=1) (bez specifikace množství se předpokládá 1 položka). Třídá má celočíselný atribut (nikoliv metodu) .total, který obsahuje celkovou cenu položek v košíku. ShoppingCart také umí vyrobit string s účtenkou (použijte a upravte řešení dřívější úlohy). Opakované přidávání téže položky bude pouze navyšovat její počet v nákupním košíku.

Řešení#

Hide code cell source
class ShoppingItem:
    def __init__(self, name: str, price: int):
        self.name = name
        self.price = price


class ShoppingCart:
    def __init__(self):
        self.items: dict[ShoppingItem, int] = {}

    def add_item(self, item: ShoppingItem, quantity: int = 1):
        if item in self.items:
            self.items[item] += quantity
        else:
            self.items[item] = quantity

    @property
    def total(self) -> int:
        return sum(i.price * q for i, q in self.items.items())
        
    def get_receipt(self, width: int = 40) -> str:
        out_str = " ÚČTENKA ".center(width, "=") + "\n"

        for item, quantity in self.items.items():
            if quantity == 1:
                fill_width = width - len(item.name)
                out_str += f"{item.name}{item.price:.>{fill_width}}\n"
            else:
                out_str += f"{item.name[:width]}\n"
                calc_str = f"{quantity} x {item.price} Kč"
                fill_width = width - len(calc_str)
                out_str += f"{calc_str}{item.price * quantity:.>{fill_width}}\n"


        out_str += (
            f"{'-':->{width}s}\n"
            f"Celkem (Kč)\n"
            f"{self.total: >{width}.2f}"
        )
        return out_str
rohlik = ShoppingItem("rohlik", 2)
avokado = ShoppingItem("avokado", 19)
maslo = ShoppingItem("maslo", 54)
olej = ShoppingItem("WD 40", 150)

cart = ShoppingCart()
cart.add_item(rohlik, 10)
cart.add_item(maslo)
cart.add_item(rohlik, 5)
cart.add_item(olej, 7)

print(cart.get_receipt())
=============== ÚČTENKA ================
rohlik
15 x 2 Kč.............................30
maslo.................................54
WD 40
7 x 150 Kč..........................1050
----------------------------------------
Celkem (Kč)
                                 1134.00

Context manager - SafeFileEdit#

Navrhněte třídu SafeFileEdit, která umožní otevřít soubor k zápisu, ale ve skutečnosti vyrobí dočasnou kopii, do které data zapíše a teprve na konci původní soubor přepíše. Třídu implementujte jako context manager.

Hide code cell source
import os
import shutil
from pathlib import Path
from typing import IO

class SafeFileEdit:
    def __init__(self, filepath):
        self.filepath = Path(filepath)
        self.tempfile_path = self.filepath.with_stem(self.filepath.stem + "tmp")
        self.handle = None

    def __enter__(self) -> IO:
        if self.filepath.exists():
            shutil.copy(self.filepath, self.tempfile_path)
        else:
            self.tempfile_path.touch()


        self.handle = open(self.tempfile_path, 'r+')
        return self.handle

    def __exit__(self, exc_type, exc_value, traceback):
        if exc_type is None:
            self.handle.close()
            with open(self.tempfile_path, 'rb') as src, open(self.filepath, 'wb') as dst:
                data = src.read()
                print(data)
                dst.write(data)
        if self.tempfile_path.exists():
            os.remove(self.tempfile_path)
with SafeFileEdit("pokus.txt") as file:
    file.write("pokus")
b'pokuspokuspokuspokuspokuspokuspokus'

Timer jako context manager#

Upravte timer z kapitoly o dekorátorech tak, aby byl použitelný jako context manager (viz příklad níže). Do konstruktoru dejte monžost výběru jednotek

Hide code cell source
import time

class Timer:
    units = {
        "s": 1,
        "ms": 1000,
        "m": 1 / 60,
        "us": 1_000_000
    }
    def __init__(self, unit="s"):
        self.start = None
        if unit not in self.units:
            print(f"unknown unit '{unit}', defaulting to 's'")
            unit = 's'
        self.unit = unit

    
    def __enter__(self):
        self.start = time.time()

    def __exit__(self, exc_type, exc_value, tracebacl):
        end = time.time()
        elapsed_time = (end - self.start) * self.units[self.unit]
        print(f"Elapsed time: {elapsed_time:.5f} {self.unit}")
def fibonacci(n):
    if n <= 0:
        return 0
    elif n == 1:
        return 1
    else:
        return fibonacci(n-1) + fibonacci(n-2)

with Timer(unit="us"):
    fibonacci(31)
    
Elapsed time: 529247.76077 us

Úlohy -řešení#

Stabilita algoritmu#

import math

i20 = 0.0043470358180281
a = 10

i0 = math.log((a+1) / a)
last = i0
print(f"I0:\t{last:+f}")
for i in range(1, 21):
    current = 1/i - a * last
    print(f"I{i}:\t{current:+f}")
    last = current
I0:	+0.095310
I1:	+0.046898
I2:	+0.031018
I3:	+0.023154
I4:	+0.018465
I5:	+0.015353
I6:	+0.013138
I7:	+0.011481
I8:	+0.010194
I9:	+0.009167
I10:	+0.008329
I11:	+0.007622
I12:	+0.007114
I13:	+0.005785
I14:	+0.013579
I15:	-0.069122
I16:	+0.753721
I17:	-7.478389
I18:	+74.839443
I19:	-748.341802
I20:	+7483.468021

V sedmnácté iteraci dostáváme záporné a tedy určitě chybné číslo, potom se výpočet úplně utrhne ze řetězu a dostáváme nesmysly. Zkusme ale výpočet obrátit, tedy:

\[ I_{n-1} = \frac{1/n - I_n}{a} \]
import math
a = 10

last = 10000
print(f"I40:\t{last:+.10f}")
for i in range(39, 19, -1):
    current = (1 / (i+1) - last) / a
    print(f"I{i}:\t{current:+.15f}")
    last = current
print(f"I{20}:\t{i20:+.15f}")
I40:	+10000.0000000000
I39:	-999.997500000000059
I38:	+100.002314102564100
I37:	-9.997599831309042
I36:	+1.002462685833607
I35:	-0.097468490805583
I34:	+0.012603991937701
I33:	+0.001680777276818
I32:	+0.002862225302621
I31:	+0.002838777469738
I30:	+0.002941928704639
I29:	+0.003039140462869
I28:	+0.003144361815782
I27:	+0.003256992389850
I26:	+0.003378004464719
I25:	+0.003508353399682
I24:	+0.003649164660032
I23:	+0.003801750200663
I22:	+0.003967651066890
I21:	+0.004148689438766
I20:	+0.004347035818028
I20:	+0.004347035818028

TODO list - řešení#

import sys

todos = []
finished = []
commands = {}

def create_todo(what):
    todos.append(what)

    
def remove_todo(index):
    todos.pop(index-1)
    
    
def finish_todo(index):
    todo = todos.pop(index-1)
    finished.append(todo)
    
    
def list_todos():
    if len(todos) > 0:
        print("\nÚKOLY\n=====")
        for i, todo in enumerate(todos):
            print(f"{i+1}: {todo}")
    else:
        print("Žádné úkoly ke splnění")
        
    if len(finished) > 0:
        print("\nHOTOVO\n======")
        for todo in finished:
            print(f"{todo}")
    print()

    
def show_help():
    print("Nápověda. Zadejte libovolný z následujících příkazů")
    for c, (_, desc, _, _) in commands.items():
        print(f"{c}: {desc}")
    print()
    
    
def quit():
    sys.exit("Páčko")


def run_command(c, arg):
    f, _, prompt, _ = commands[c]
    if prompt is None:
        f()
    else:
        f(arg)     
        
def parse_command():
    c = input("> ")
    if c not in commands:
        print(f"Neznámý příkaz: {c}")
        return
    
    f, _, prompt, type_ = commands[c]
    
    arg = None
    if prompt is not None:
        arg = type_(input(prompt))
    
    run_command(c, arg)

    
commands = {
    "c": (create_todo, "vytvořit nový úkol", "Zadejte nový úkol: ", str),
    "r": (remove_todo, "odstranit úkol", "Zadejte číslo úkolu k odstranění: ", int),
    "f": (finish_todo, "ukončit úkol", "Zadejte číslo úkolu k ukončení: ", int),
    "l": (list_todos, "vypsat úkoly", None, None),
    "h": (show_help, "zobrazit nápovědu", None, None),
    "q": (quit, "opustit program", None, None),
}    
    
    
record = [
    ("c", "vyprat"),
    ("c", "vysát"),
    ("c", "nakoupit"),
    ("r", 2),
    ("c", "dodělat přednášku na python"),
    ("f", 1),
    ("f", 1),
    ("l", None),
    ("h", None)
]    

for c, arg in record:
    run_command(c, arg)

# while True:  # zakomentováno kvůli kompilaci knihy
#     parse_command()
ÚKOLY
=====
1: dodělat přednášku na python

HOTOVO
======
vyprat
nakoupit

Nápověda. Zadejte libovolný z následujících příkazů
c: vytvořit nový úkol
r: odstranit úkol
f: ukončit úkol
l: vypsat úkoly
h: zobrazit nápovědu
q: opustit program
# jednotna struktura command + odchyt chyb

import sys

todos = []
finished = []
commands = {}

def create_todo(*args):
    try:
        what = str(args[0])
    except ValueError:
        print(f"Neplatný argument: {args[0]}")
        return
    todos.append(what)

    
def remove_todo(*args):
    try:
        index = int(args[0])
    except ValueError:
        print(f"Zadejte číslo: {args[0]}")
        return
    todos.pop(index-1)
    
    
def finish_todo(*args):
    try:
        index = int(args[0])
    except ValueError:
        print(f"Zadejte číslo: {args[0]}")
        return
    todo = todos.pop(index-1)
    finished.append(todo)
    
    
def list_todos(*args):
    if len(todos) > 0:
        print("\nÚKOLY\n=====")
        for i, todo in enumerate(todos):
            print(f"{i+1}: {todo}")
    else:
        print("\nŽádné úkoly ke splnění")
        
    if len(finished) > 0:
        print("\nHOTOVO\n======")
        for todo in finished:
            print(f"{todo}")
    print()

    
def show_help(*args):
    print("Nápověda. Zadejte libovolný z následujících příkazů")
    for c, (_, desc, _) in commands.items():
        print(f"{c}: {desc}")
    print()
    
    
def quit(*args):
    sys.exit("Páčko")


def run_command(c, arg):
    f, _, prompt = commands[c]
    if prompt is None:
        f()
    else:
        f(arg)     
        
def parse_command():
    c = input("> ")
    if c not in commands:
        print(f"Neznámý příkaz: {c}")
        return
    
    f, _, prompt = commands[c]
    
    arg = None
    if prompt is not None:
        arg = input(prompt)
    
    run_command(c, arg)

    
commands = {
    "c": (create_todo, "vytvořit nový úkol", "Zadejte nový úkol: "),
    "r": (remove_todo, "odstranit úkol", "Zadejte číslo úkolu k odstranění: "),
    "f": (finish_todo, "ukončit úkol", "Zadejte číslo úkolu k ukončení: "),
    "l": (list_todos, "vypsat úkoly", None),
    "h": (show_help, "zobrazit nápovědu", None),
    "q": (quit, "opustit program", None),
}    
    
    
record = [
    ("c", "vyprat"),
    ("c", "vysát"),
    ("c", "nakoupit"),
    ("r", 2),
    ("c", "dodělat přednášku na python"),
    ("f", 1),
    ("f", 1),
    ("l", None),
    ("h", None)
]    

for c, arg in record:
    run_command(c, arg)

# while True:  # zakomentováno kvůli kompilaci knihy
#     parse_command()
ÚKOLY
=====
1: dodělat přednášku na python

HOTOVO
======
vyprat
nakoupit

Nápověda. Zadejte libovolný z následujících příkazů
c: vytvořit nový úkol
r: odstranit úkol
f: ukončit úkol
l: vypsat úkoly
h: zobrazit nápovědu
q: opustit program

Literatura#

[1]

Two's complement. https://en.wikipedia.org/wiki/Two's_complement. Accessed: 2024-02-18.

[2]

Ariane flight v88. https://en.wikipedia.org/wiki/Ariane_flight_V88. Accessed: 2024-02-19.

[3]

How gangnam style broke youtube. https://youtu.be/vA0Rl6Ne5C8. Accessed: 2024-02-19.

[4]

Python string formatting best practices. https://realpython.com/python-string-formatting/#4-template-strings-standard-library. Accessed: 2024-02-19.

[5]

Python 3.8 documentation. https://docs.python.org/3.8/. Accessed: 2024-02-10.